From e73b0f7449a2b3c462974d5b0c7c005552ef49ca Mon Sep 17 00:00:00 2001 From: Nightapes Date: Tue, 23 Oct 2018 17:48:16 +0200 Subject: [PATCH 1/3] temp(email): add first part of email check --- package.json | 1 + src/components/email/email-util.spec.ts | 31 +++++++++ src/components/email/email-util.ts | 84 +++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/components/email/email-util.spec.ts create mode 100644 src/components/email/email-util.ts diff --git a/package.json b/package.json index 41daf00..a4b6c97 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "test-watch": "karma start --no-single-run --auto-watch", "build": "tslint --project . && ng-packagr -p package.json", "commit": "tslint --project . && npm test && git-cz", + "lint": "tslint --project .", "semantic-release": "semantic-release", "travis-deploy-once": "travis-deploy-once" }, diff --git a/src/components/email/email-util.spec.ts b/src/components/email/email-util.spec.ts new file mode 100644 index 0000000..f63bf6a --- /dev/null +++ b/src/components/email/email-util.spec.ts @@ -0,0 +1,31 @@ +import { EmailValidators } from './email-validators'; +import { FormControl } from '@angular/forms'; +import { EmailSuggestion } from './email-util'; + +fdescribe('Email util service', () => { + describe('check split', () => { + + let emailSuggestion: EmailSuggestion = new EmailSuggestion(); + + it('should work splitting emails', () => { + const mail = [ + 'prettyandsimple@example.com', + 'very.common@example.com', + 'disposable.style.email.with+symbol@example.com', + 'other.email-with-dash@example.com', + 'x@example.com', + 'example@s.solutions', + 'example-indeed@strange-example.com', + + ]; + + mail.forEach(element => { + let result = emailSuggestion.splitEmail(element) + console.log(result) + expect(result).toBeDefined(); + }); + + }); + + }); +}); \ No newline at end of file diff --git a/src/components/email/email-util.ts b/src/components/email/email-util.ts new file mode 100644 index 0000000..e7407e6 --- /dev/null +++ b/src/components/email/email-util.ts @@ -0,0 +1,84 @@ + +export interface EmailOptions { + domains: string[], + secondLevelDomains: string[], + topLevelDomains: string[] +} + +export interface SplittedEmail { + topLevelDomain: string, + secondLevelDomain: string, + domain: string, + address: string +} + +export class EmailSuggestion { + + private defaultOptions: EmailOptions = { + domains: ['msn.com', 'bellsouth.net', + 'telus.net', 'comcast.net', 'optusnet.com.au', + 'earthlink.net', 'qq.com', 'sky.com', 'icloud.com', + 'mac.com', 'sympatico.ca', 'googlemail.com', + 'att.net', 'xtra.co.nz', 'web.de', + 'cox.net', 'gmail.com', 'ymail.com', + 'aim.com', 'rogers.com', 'verizon.net', + 'rocketmail.com', 'google.com', 'optonline.net', + 'sbcglobal.net', 'aol.com', 'me.com', 'btinternet.com', + 'charter.net', 'shaw.ca'], + secondLevelDomains: ["yahoo", "hotmail", "mail", "live", "outlook", "gmx"], + topLevelDomains: ["com", "com.au", "com.tw", "ca", "co.nz", "co.uk", "de", + "fr", "it", "ru", "net", "org", "edu", "gov", "jp", "nl", "kr", "se", "eu", + "ie", "co.il", "us", "at", "be", "dk", "hk", "es", "gr", "ch", "no", "cz", + "in", "net", "net.au", "info", "biz", "mil", "co.jp", "sg", "hu", "uk"] + } + + public setDefaults(options: EmailOptions) { + this.defaultOptions = options + } + + public suggest(email: string) { + let emailParts = this.splitEmail(email.toLowerCase()) + if (!emailParts) { + + } + + }; + + public splitEmail(email: string) { + + var parts = email.trim().split('@'); + + if (parts.length != 2) { + return undefined; + } + + if (parts[0] === '' || parts[1] === '') { + return undefined; + } + + let result = { + topLevelDomain: "", + secondLevelDomain: "", + domain: parts[1], + address: parts[0] + } + + let domainParts = parts[1].split('.'); + + if (domainParts.length === 0) { + return undefined; + } else if (domainParts.length == 1) { + result.topLevelDomain = domainParts[0]; + } else { + // The address has a domain and a top-level domain + result.secondLevelDomain = domainParts[0]; + for (var j = 1; j < domainParts.length; j++) { + result.topLevelDomain += domainParts[j] + '.'; + } + result.topLevelDomain = result.topLevelDomain.substring(0, result.topLevelDomain.length - 1); + } + + return result; + + } +} \ No newline at end of file From a2efa83f315d9249746ba1e8133bcbb2e6f17608 Mon Sep 17 00:00:00 2001 From: Nightapes Date: Sat, 23 Feb 2019 19:08:08 +0100 Subject: [PATCH 2/3] feat(mailcheck ): add email suggestion validator, closes #44 --- README.md | 13 +- package.json | 147 ++++++++------- src/components/email/email-util.spec.ts | 207 ++++++++++++++++++--- src/components/email/email-util.ts | 225 +++++++++++++++++++++-- src/components/email/email-validators.ts | 25 ++- src/components/email/email.directive.ts | 24 +++ src/components/validators.module.ts | 6 +- src/public_api.ts | 3 +- 8 files changed, 533 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index a7d8bbc..62e6f16 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ import {EmailValidators} from 'ngx-validators' email: FormControl = new FormControl('', EmailValidators.normal); email2: FormControl = new FormControl('', EmailValidators.simple); +email3: FormControl = new FormControl('', EmailValidators.suggest); ``` ### Universal @@ -247,12 +248,23 @@ export class AppModule { ``` ### Email + +#### Normal + ```html
Is not a email
+``` + +#### Suggest +```html +
+ +Maybe check the mail again +
``` ### Universal @@ -306,7 +318,6 @@ export class AppModule { ##Todo -* Implement https://github.com/mailcheck/mailcheck * Add more password rules * Add address validator diff --git a/package.json b/package.json index 43dd3ed..e325e80 100644 --- a/package.json +++ b/package.json @@ -1,77 +1,76 @@ { - "name": "ngx-validators", - "version": "0.0.0-development", - "description": "An implementation of angular validators for Angular 2 and higher", - "scripts": { - "test": "karma start", - "test-watch": "karma start --no-single-run --auto-watch", - "build": "tslint --project . && ng-packagr -p package.json", - "lint": "tslint --project .", - "commit": "tslint --project . && npm test && git-cz", + "name": "ngx-validators", + "version": "0.0.0-development", + "description": "An implementation of angular validators for Angular 2 and higher", + "scripts": { + "test": "karma start", + "test-watch": "karma start --no-single-run --auto-watch", + "build": "tslint --project . && ng-packagr -p package.json", + "commit": "tslint --project . && npm test && git-cz", "lint": "tslint --project .", - "semantic-release": "semantic-release", - "travis-deploy-once": "travis-deploy-once" - }, - "repository": { - "type": "git", - "url": "https://github.com/Nightapes/ngx-validators.git" - }, - "keywords": [ - "angular", - "angular2+", - "validators" - ], - "author": "Sebastian Beisch", - "license": "MIT", - "bugs": { - "url": "https://github.com/Nightapes/ngx-validators/issues" - }, - "homepage": "https://github.com/Nightapes/ngx-validators", - "peerDependencies": { - "@angular/common": ">= 2.0.0", - "@angular/core": ">= 2.0.0", - "@angular/forms": ">= 2.0.0" - }, - "devDependencies": { - "@angular/cli": "^7.0.2", - "@angular/common": "^7.0.0", - "@angular/compiler": "^7.0.0", - "@angular/compiler-cli": "^7.0.0", - "@angular/core": "^7.0.0", - "@angular/forms": "^7.0.0", - "@angular/platform-browser": "^7.0.0", - "@angular/platform-browser-dynamic": "^7.0.0", - "@types/google-libphonenumber": "^7.4.10", - "@types/jasmine": "2.8.8", - "@types/node": "^10.9.4", - "codelyzer": "^4.2.1", - "commitizen": "2.10.1", - "cz-conventional-changelog": "2.1.0", - "jasmine-core": "3.2.1", - "karma": "3.0.0", - "karma-chrome-launcher": "2.2.0", - "karma-jasmine": "1.1.2", - "karma-typescript": "^3.0.12", - "ng-packagr": "^4.1.1", - "rxjs": "^6.2.1", - "rxjs-compat": "^6.2.1", - "semantic-release": "^15.9.14", - "travis-deploy-once": "^5.0.7", - "tsickle": "^0.33.1", - "tslib": "^1.9.3", - "tslint": "^5.9.1", - "typescript": "3.1.3", - "zone.js": "^0.8.26" - }, - "config": { - "commitizen": { - "path": "node_modules/cz-conventional-changelog" + "semantic-release": "semantic-release", + "travis-deploy-once": "travis-deploy-once" + }, + "repository": { + "type": "git", + "url": "https://github.com/Nightapes/ngx-validators.git" + }, + "keywords": [ + "angular", + "angular2+", + "validators" + ], + "author": "Sebastian Beisch", + "license": "MIT", + "bugs": { + "url": "https://github.com/Nightapes/ngx-validators/issues" + }, + "homepage": "https://github.com/Nightapes/ngx-validators", + "peerDependencies": { + "@angular/common": ">= 2.0.0", + "@angular/core": ">= 2.0.0", + "@angular/forms": ">= 2.0.0" + }, + "devDependencies": { + "@angular/cli": "^7.0.2", + "@angular/common": "^7.0.0", + "@angular/compiler": "^7.0.0", + "@angular/compiler-cli": "^7.0.0", + "@angular/core": "^7.0.0", + "@angular/forms": "^7.0.0", + "@angular/platform-browser": "^7.0.0", + "@angular/platform-browser-dynamic": "^7.0.0", + "@types/google-libphonenumber": "^7.4.10", + "@types/jasmine": "2.8.8", + "@types/node": "^10.9.4", + "codelyzer": "^4.2.1", + "commitizen": "2.10.1", + "cz-conventional-changelog": "2.1.0", + "jasmine-core": "3.2.1", + "karma": "3.0.0", + "karma-chrome-launcher": "2.2.0", + "karma-jasmine": "1.1.2", + "karma-typescript": "^3.0.12", + "ng-packagr": "^4.1.1", + "rxjs": "^6.2.1", + "rxjs-compat": "^6.2.1", + "semantic-release": "^15.9.14", + "travis-deploy-once": "^5.0.7", + "tsickle": "^0.33.1", + "tslib": "^1.9.3", + "tslint": "^5.9.1", + "typescript": "3.1.3", + "zone.js": "^0.8.26" + }, + "config": { + "commitizen": { + "path": "node_modules/cz-conventional-changelog" + } + }, + "dependencies": {}, + "ngPackage": { + "lib": { + "entryFile": "./src/public_api.ts" + } } - }, - "dependencies": {}, - "ngPackage": { - "lib": { - "entryFile": "./src/public_api.ts" - } - } -} +} \ No newline at end of file diff --git a/src/components/email/email-util.spec.ts b/src/components/email/email-util.spec.ts index f63bf6a..e73ee61 100644 --- a/src/components/email/email-util.spec.ts +++ b/src/components/email/email-util.spec.ts @@ -1,31 +1,194 @@ -import { EmailValidators } from './email-validators'; -import { FormControl } from '@angular/forms'; -import { EmailSuggestion } from './email-util'; +import { EmailSuggestion, SplittedEmail, EmailOptions } from './email-util'; -fdescribe('Email util service', () => { - describe('check split', () => { +interface SplitEmailTest { + test: string, + result: SplittedEmail +} - let emailSuggestion: EmailSuggestion = new EmailSuggestion(); +describe('Email util service', () => { - it('should work splitting emails', () => { - const mail = [ - 'prettyandsimple@example.com', - 'very.common@example.com', - 'disposable.style.email.with+symbol@example.com', - 'other.email-with-dash@example.com', - 'x@example.com', - 'example@s.solutions', - 'example-indeed@strange-example.com', + let emailSuggestion: EmailSuggestion = new EmailSuggestion(); - ]; - - mail.forEach(element => { - let result = emailSuggestion.splitEmail(element) - console.log(result) - expect(result).toBeDefined(); - }); + it('should work splitting emails', () => { + const mail: SplitEmailTest[] = [{ + test: 'prettyandsimple@example.com', + result: { + topLevelDomain: 'com', + secondLevelDomain: 'example', + domain: 'example.com', + address: 'prettyandsimple' + } + }, { + test: 'very.common@example.com', + result: { + topLevelDomain: 'com', + secondLevelDomain: 'example', + domain: 'example.com', + address: 'very.common' + } + }, { + test: 'disposable.style.email.with+symbol@example.com', + result: { + topLevelDomain: 'com', + secondLevelDomain: 'example', + domain: 'example.com', + address: 'disposable.style.email.with+symbol' + } + }, { + test: 'other.email-with-dash@example.com', + result: { + topLevelDomain: 'com', + secondLevelDomain: 'example', + domain: 'example.com', + address: 'other.email-with-dash' + } + }, { + test: 'example@s.solutions', + result: { + topLevelDomain: 'solutions', + secondLevelDomain: 's', + domain: 's.solutions', + address: 'example' + } + }, { + test: 'example-indeed@strange-example.com', + result: { + topLevelDomain: 'com', + secondLevelDomain: 'strange-example', + domain: 'strange-example.com', + address: 'example-indeed' + } + }, { + test: '"()<>[]:;@,\\\"!#$%&\'*+-/=?^_`{}|\ \ \ \ \ ~\ \ \ \ \ \ \ ?\ \ \ \ \ \ \ \ \ \ \ \ ^_`{}|~.a"@allthesymbols.com', + result: { + topLevelDomain: 'com', + secondLevelDomain: 'allthesymbols', + domain: 'allthesymbols.com', + address: '"()<>[]:;@,\\\"!#$%&\'*+-/=?^_`{}|\ \ \ \ \ ~\ \ \ \ \ \ \ ?\ \ \ \ \ \ \ \ \ \ \ \ ^_`{}|~.a"' + } + }, { + test: 'postbox@com', + result: { + topLevelDomain: 'com', + secondLevelDomain: '', + domain: 'com', + address: 'postbox' + } + }, { + test: '"foo@bar"@example.com', + result: { + topLevelDomain: 'com', + secondLevelDomain: 'example', + domain: 'example.com', + address: '"foo@bar"' + } + }, { + test: 'test@mail.randomsmallcompany.co.uk', + result: { + topLevelDomain: 'randomsmallcompany.co.uk', + secondLevelDomain: 'mail', + domain: 'mail.randomsmallcompany.co.uk', + address: 'test' + } + }, { + test: '"contains.and.@.symbols.com"@example.com', + result: { + topLevelDomain: 'com', + secondLevelDomain: 'example', + domain: 'example.com', + address: '"contains.and.@.symbols.com"' + } + }, { + test: 'example-indeed-broken-strange-example.com', + result: undefined + }, { + test: '@', + result: undefined + }, { + test: 'email@', + result: undefined + }, { + test: '@example.com', + result: undefined + } + ]; + mail.forEach(element => { + let result = emailSuggestion.splitEmail(element.test) + expect(result).toEqual(element.result); }); }); + + // expect(mailcheck.suggest('', domains)).toBeFalsy(); + // expect(mailcheck.suggest('test@', domains)).toBeFalsy(); + // expect(mailcheck.suggest('test', domains)).toBeFalsy(); + + it('should work suggest emails', () => { + testSuggestion('gmailc.om', 'gmail.com') + testSuggestion('gmailc.om', 'gmail.com'); + testSuggestion('emaildomain.co', 'emaildomain.com'); + testSuggestion('gmail.con', 'gmail.com'); + testSuggestion('gnail.con', 'gmail.com'); + testSuggestion('GNAIL.con', 'gmail.com'); + testSuggestion('#gmail.com', 'gmail.com'); + testSuggestion('comcast.nry', 'comcast.net'); + testSuggestion('homail.con', 'hotmail.com'); + testSuggestion('hotmail.co', 'hotmail.com'); + testSuggestion('yajoo.com', 'yahoo.com'); + testSuggestion('yajoo.de', 'yahoo.de'); + testSuggestion('randomsmallcompany.cmo', 'randomsmallcompany.com'); + + // Ensure we do not touch the second level domain when suggesting new top level domain + testSuggestion('con-artists.con', 'con-artists.com'); + }); + + it('should work suggest emails for broken mails', () => { + + testSuggestion('', undefined); + testSuggestion('test@', undefined); + testSuggestion('test', undefined); + }); + + it('should work suggest emails own options', () => { + + const options: EmailOptions = { + domains: ['gmail.com'], + secondLevelDomains: ['gmail'], + topLevelDomains: ['com'] + } + testSuggestion('gmailc.om', 'gmail.com', options); + }); + + it('should work suggest emails empty options', () => { + + const options: EmailOptions = { + domains: ['gmail.com'], + secondLevelDomains: [], + topLevelDomains: [] + } + testSuggestion('gmailc.om', 'gmail.com', options); + }); + + function testSuggestion(testDomain: string, resultDomain: any, options?: EmailOptions) { + + const address = 'test' + let result + + if (options) { + result = emailSuggestion.suggest(address + '@' + testDomain, options) + } else { + result = emailSuggestion.suggest(address + '@' + testDomain) + } + + if (resultDomain) { + expect(result.suggestion.domain).toEqual(resultDomain); + expect(result.suggestion.address).toEqual(address); + expect(result.suggestion.full).toEqual(address + '@' + resultDomain); + } else { + expect(result).toEqual(resultDomain); + } + + } + }); \ No newline at end of file diff --git a/src/components/email/email-util.ts b/src/components/email/email-util.ts index e7407e6..c9373f9 100644 --- a/src/components/email/email-util.ts +++ b/src/components/email/email-util.ts @@ -1,3 +1,14 @@ +import { Injectable } from "@angular/core"; + +/* + * Code fromMailcheck https://github.com/mailcheck/mailcheck + * Author + * Derrick Ko (@derrickko) + * + * Released under the MIT License. + * + * v 1.1.2 + */ export interface EmailOptions { domains: string[], @@ -12,6 +23,18 @@ export interface SplittedEmail { address: string } +export interface Suggestion { + address: string, + domain: string, + full: string +} + +interface Offset { + c1: number, + c2: number, + trans: boolean +} + export class EmailSuggestion { private defaultOptions: EmailOptions = { @@ -20,7 +43,7 @@ export class EmailSuggestion { 'earthlink.net', 'qq.com', 'sky.com', 'icloud.com', 'mac.com', 'sympatico.ca', 'googlemail.com', 'att.net', 'xtra.co.nz', 'web.de', - 'cox.net', 'gmail.com', 'ymail.com', + 'cox.net', 'gmail.com', 'ymail.com', 'yahoo.com', 'aim.com', 'rogers.com', 'verizon.net', 'rocketmail.com', 'google.com', 'optonline.net', 'sbcglobal.net', 'aol.com', 'me.com', 'btinternet.com', @@ -32,38 +55,89 @@ export class EmailSuggestion { "in", "net", "net.au", "info", "biz", "mil", "co.jp", "sg", "hu", "uk"] } - public setDefaults(options: EmailOptions) { - this.defaultOptions = options - } + public suggest(email: string, options?: EmailOptions): { [key: string]: Suggestion } { + let opt = this.defaultOptions; + if (options != undefined) { + opt = options; + } + let emailParts = this.splitEmail(email.toLowerCase()); - public suggest(email: string) { - let emailParts = this.splitEmail(email.toLowerCase()) if (!emailParts) { + return undefined; + } + if (opt.secondLevelDomains && opt.topLevelDomains) { + // If the email is a valid 2nd-level + top-level, do not suggest anything. + if (opt.secondLevelDomains.indexOf(emailParts.secondLevelDomain) !== -1 && opt.topLevelDomains.indexOf(emailParts.topLevelDomain) !== -1) { + return undefined; + } + } + + let closestDomain = this.findClosestDomain(emailParts.domain, opt.domains, 2); + if (closestDomain) { + if (closestDomain == emailParts.domain) { + // The email address exactly matches one of the supplied domains; do not return a suggestion. + return undefined; + } else { + // The email address closely matches one of the supplied domains; return a suggestion + return { suggestion: { address: emailParts.address, domain: closestDomain, full: emailParts.address + "@" + closestDomain } }; + } } + let closestSecondLevelDomain = this.findClosestDomain(emailParts.secondLevelDomain, opt.secondLevelDomains, 2); + let closestTopLevelDomain = this.findClosestDomain(emailParts.topLevelDomain, opt.topLevelDomains, 2); + + if (emailParts.domain) { + closestDomain = emailParts.domain; + let rtrn = false; + + if (closestSecondLevelDomain && closestSecondLevelDomain != emailParts.secondLevelDomain) { + // The email address may have a mispelled second-level domain; return a suggestion + closestDomain = closestDomain.replace(emailParts.secondLevelDomain, closestSecondLevelDomain); + rtrn = true; + } + + if (closestTopLevelDomain && closestTopLevelDomain != emailParts.topLevelDomain && emailParts.secondLevelDomain !== '') { + // The email address may have a mispelled top-level domain; return a suggestion + closestDomain = closestDomain.replace(new RegExp(emailParts.topLevelDomain + "$"), closestTopLevelDomain); + rtrn = true; + } + + if (rtrn) { + return { suggestion: { address: emailParts.address, domain: closestDomain, full: emailParts.address + "@" + closestDomain } }; + } + } + + /* The email address exactly matches one of the supplied domains, does not closely + * match any domain and does not appear to simply have a mispelled top-level domain, + * or is an invalid email address; do not return a suggestion. + */ + return undefined; + }; public splitEmail(email: string) { - var parts = email.trim().split('@'); + let parts = email.trim().split('@'); - if (parts.length != 2) { + if (parts.length < 2) { return undefined; } - if (parts[0] === '' || parts[1] === '') { - return undefined; + for (var i = 0; i < parts.length; i++) { + if (parts[i] === '') { + return undefined; + } } let result = { topLevelDomain: "", secondLevelDomain: "", - domain: parts[1], - address: parts[0] + domain: parts.pop(), + address: '' } - let domainParts = parts[1].split('.'); + let domainParts = result.domain.split('.'); if (domainParts.length === 0) { return undefined; @@ -72,13 +146,136 @@ export class EmailSuggestion { } else { // The address has a domain and a top-level domain result.secondLevelDomain = domainParts[0]; - for (var j = 1; j < domainParts.length; j++) { + for (let j = 1; j < domainParts.length; j++) { result.topLevelDomain += domainParts[j] + '.'; } result.topLevelDomain = result.topLevelDomain.substring(0, result.topLevelDomain.length - 1); } + result.address = parts.join('@'); + return result; } + + private findClosestDomain(domain: string, domains: string[], threshold: number): string { + let dist; + let minDist = Infinity; + let closestDomain = null; + + if (!domain || !domains) { + return undefined; + } + + for (let i = 0; i < domains.length; i++) { + if (domain === domains[i]) { + return domain; + } + dist = this.sift4Distance(domain, domains[i], 5); + if (dist < minDist) { + minDist = dist; + closestDomain = domains[i]; + } + } + + if (minDist <= threshold && closestDomain !== null) { + return closestDomain; + } else { + return undefined; + } + } + + private sift4Distance(s1: string, s2: string, maxOffset: number): number { + // sift4: https://siderite.blogspot.com/2014/11/super-fast-and-accurate-string-distance.html + if (maxOffset === undefined) { + maxOffset = 5; //default + } + + if (!s1 || !s1.length) { + if (!s2) { + return 0; + } + return s2.length; + } + + if (!s2 || !s2.length) { + return s1.length; + } + + let l1 = s1.length; + let l2 = s2.length; + + let c1 = 0; //cursor for string 1 + let c2 = 0; //cursor for string 2 + let lcss = 0; //largest common subsequence + let local_cs = 0; //local common substring + let trans = 0; //number of transpositions ('ab' vs 'ba') + let offset_arr: Offset[] = []; //offset pair array, for computing the transpositions + + while ((c1 < l1) && (c2 < l2)) { + if (s1.charAt(c1) == s2.charAt(c2)) { + local_cs++; + let isTrans = false; + //see if current match is a transposition + let i = 0; + while (i < offset_arr.length) { + let ofs = offset_arr[i]; + if (c1 <= ofs.c1 || c2 <= ofs.c2) { + // when two matches cross, the one considered a transposition is the one with the largest difference in offsets + isTrans = Math.abs(c2 - c1) >= Math.abs(ofs.c2 - ofs.c1); + if (isTrans) { + trans++; + } else { + if (!ofs.trans) { + ofs.trans = true; + trans++; + } + } + break; + } else { + if (c1 > ofs.c2 && c2 > ofs.c1) { + offset_arr.splice(i, 1); + } else { + i++; + } + } + } + offset_arr.push({ + c1: c1, + c2: c2, + trans: isTrans + }); + } else { + lcss += local_cs; + local_cs = 0; + if (c1 != c2) { + c1 = c2 = Math.min(c1, c2); //using min allows the computation of transpositions + } + //if matching characters are found, remove 1 from both cursors (they get incremented at the end of the loop) + //so that we can have only one code block handling matches + for (let j = 0; j < maxOffset && (c1 + j < l1 || c2 + j < l2); j++) { + if ((c1 + j < l1) && (s1.charAt(c1 + j) == s2.charAt(c2))) { + c1 += j - 1; + c2--; + break; + } + if ((c2 + j < l2) && (s1.charAt(c1) == s2.charAt(c2 + j))) { + c1--; + c2 += j - 1; + break; + } + } + } + c1++; + c2++; + // this covers the case where the last match is on the last token in list, so that it can compute transpositions correctly + if ((c1 >= l1) || (c2 >= l2)) { + lcss += local_cs; + local_cs = 0; + c1 = c2 = Math.min(c1, c2); + } + } + lcss += local_cs; + return Math.round(Math.max(l1, l2) - lcss + trans); //add the cost of transpositions to the final result + } } \ No newline at end of file diff --git a/src/components/email/email-validators.ts b/src/components/email/email-validators.ts index 8e23f9e..629787a 100644 --- a/src/components/email/email-validators.ts +++ b/src/components/email/email-validators.ts @@ -1,9 +1,15 @@ -import { AbstractControl } from '@angular/forms'; +import { AbstractControl, ValidatorFn } from '@angular/forms'; import { AbstractControlUtil } from './../abstract-control-util'; +import { EmailSuggestion, EmailOptions } from './email-util'; export class EmailValidators { + private static emailSuggestion: EmailSuggestion = new EmailSuggestion(); + public static simple(control: AbstractControl): { [key: string]: boolean } { - if (AbstractControlUtil.isNotPresent(control)) return undefined; + if (AbstractControlUtil.isNotPresent(control)) { + return undefined + }; + let pattern = /.+@.+\..+/i; if (pattern.test(control.value)) { return undefined; @@ -13,7 +19,9 @@ export class EmailValidators { // https://www.w3.org/TR/html5/forms.html#valid-e-mail-address public static normal(control: AbstractControl): { [key: string]: boolean } { - if (AbstractControlUtil.isNotPresent(control)) return undefined; + if (AbstractControlUtil.isNotPresent(control)) { + return undefined + }; // tslint:disable-next-line:max-line-length let pattern = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; if (pattern.test(control.value)) { @@ -21,4 +29,15 @@ export class EmailValidators { } return { 'normalEmailRule': true }; }; + + public static suggest(options?: EmailOptions): ValidatorFn { + const validator = (control: AbstractControl): { [key: string]: any } => { + if (AbstractControlUtil.isNotPresent(control)) { + return undefined + }; + return this.emailSuggestion.suggest(control.value, options) + }; + return validator; + }; + } diff --git a/src/components/email/email.directive.ts b/src/components/email/email.directive.ts index f85e5b1..799394f 100644 --- a/src/components/email/email.directive.ts +++ b/src/components/email/email.directive.ts @@ -1,3 +1,4 @@ +import { EmailSuggestion, EmailOptions } from './email-util'; import { Directive, Input, forwardRef, OnInit } from '@angular/core'; import { NG_VALIDATORS, Validator, ValidatorFn, AbstractControl } from '@angular/forms'; @@ -36,3 +37,26 @@ export class EmailValidatorDirective implements Validator, OnInit { return this.validator(c); } } + +@Directive({ + selector: '[emailSuggest][formControlName],[emailSuggest][formControl],[emailSuggest][ngModel]', + providers: [{ + provide: NG_VALIDATORS, + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => EmailSuggestValidatorDirective), + multi: true + }] +}) +export class EmailSuggestValidatorDirective implements Validator, OnInit { + @Input() emailSuggest: EmailOptions; + + private validator: ValidatorFn; + + ngOnInit() { + this.validator = EmailValidators.suggest(this.emailSuggest); + } + + validate(c: AbstractControl): { [key: string]: any } { + return this.validator(c); + } +} diff --git a/src/components/validators.module.ts b/src/components/validators.module.ts index 214eb47..3749d68 100644 --- a/src/components/validators.module.ts +++ b/src/components/validators.module.ts @@ -9,7 +9,7 @@ import { import { NgModule } from '@angular/core'; import { CreditCardValidatorDirective } from './creditcard/creditcard.directive'; -import { EmailValidatorDirective } from './email/email.directive'; +import { EmailValidatorDirective, EmailSuggestValidatorDirective } from './email/email.directive'; import { PasswordValidatorDirective } from './password/password.directive'; import { EqualToDirective } from './equal-to/equal-to.directive'; @@ -17,6 +17,7 @@ import { EqualToDirective } from './equal-to/equal-to.directive'; declarations: [ CreditCardValidatorDirective, EmailValidatorDirective, + EmailSuggestValidatorDirective, PasswordValidatorDirective, IsInRangeValidatorDirective, IsNumberValidatorDirective, @@ -29,6 +30,7 @@ import { EqualToDirective } from './equal-to/equal-to.directive'; exports: [ CreditCardValidatorDirective, EmailValidatorDirective, + EmailSuggestValidatorDirective, PasswordValidatorDirective, IsInRangeValidatorDirective, IsNumberValidatorDirective, @@ -39,4 +41,4 @@ import { EqualToDirective } from './equal-to/equal-to.directive'; EqualToDirective ] }) -export class ValidatorsModule {} +export class ValidatorsModule { } diff --git a/src/public_api.ts b/src/public_api.ts index 5c81711..7e2a944 100644 --- a/src/public_api.ts +++ b/src/public_api.ts @@ -1,12 +1,13 @@ // validators export { PasswordValidators } from './components/password/password-validators'; export { EmailValidators } from './components/email/email-validators'; +export * from './components/email/email-util'; export { UniversalValidators } from './components/universal/universal-validators'; export { CreditCardValidators } from './components/creditcard/creditcard-validators'; //Directive export { PasswordValidatorDirective } from './components/password/password.directive'; -export { EmailValidatorDirective } from './components/email/email.directive'; +export { EmailValidatorDirective, EmailSuggestValidatorDirective } from './components/email/email.directive'; export { IsInRangeValidatorDirective, IsNumberValidatorDirective, MaxValidatorDirective, MinValidatorDirective, WhiteSpaceValidatorDirective, EmptyStringValidatorDirective From 0a2d344930a41ddb74fdb01a5e71ffc979e1e742 Mon Sep 17 00:00:00 2001 From: Nightapes Date: Sat, 23 Feb 2019 19:08:42 +0100 Subject: [PATCH 3/3] docs(mailcheck): add examples for email suggestion --- .../email-validator.component.html | 4 ++ .../email-validator/email-validator.module.ts | 8 +++- .../form/email/form-email.component.html | 2 +- .../suggest/form-email-suggest.component.html | 6 +++ .../suggest/form-email-suggest.component.ts | 34 +++++++++++++++ .../email/reactive-form-email.component.ts | 4 ++ ...reactive-form-email-suggest.component.html | 6 +++ .../reactive-form-email-suggest.component.ts | 43 +++++++++++++++++++ examples/src/app/items.ts | 18 +++++--- .../util/codeviewer/codeviewer.component.ts | 6 +-- examples/src/styles.css | 5 +++ 11 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 examples/src/app/email-validator/form/suggest/form-email-suggest.component.html create mode 100644 examples/src/app/email-validator/form/suggest/form-email-suggest.component.ts create mode 100644 examples/src/app/email-validator/reactive-form/suggest/reactive-form-email-suggest.component.html create mode 100644 examples/src/app/email-validator/reactive-form/suggest/reactive-form-email-suggest.component.ts diff --git a/examples/src/app/email-validator/email-validator.component.html b/examples/src/app/email-validator/email-validator.component.html index d772a19..ee31b92 100644 --- a/examples/src/app/email-validator/email-validator.component.html +++ b/examples/src/app/email-validator/email-validator.component.html @@ -15,9 +15,13 @@ + +
Try test@gmai.con
+ +
Try test@gmai.con
diff --git a/examples/src/app/email-validator/email-validator.module.ts b/examples/src/app/email-validator/email-validator.module.ts index d836559..893d37d 100644 --- a/examples/src/app/email-validator/email-validator.module.ts +++ b/examples/src/app/email-validator/email-validator.module.ts @@ -1,5 +1,5 @@ import { MatCardModule, MatListModule, MatFormFieldModule, MatTooltipModule, MatInputModule, MatSelectModule } from '@angular/material'; -import { NgModule, Component } from '@angular/core'; +import { NgModule, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule, MatTabsModule } from '@angular/material'; import { Routes, RouterModule } from '@angular/router'; @@ -10,6 +10,8 @@ import { ReactiveFormEmailComponent } from './reactive-form/email/reactive-form- import { FormEmailComponent } from './form/email/form-email.component'; import { ValidatorsModule } from 'ngx-validators'; +import { ReactiveFormEmailSuggestComponent } from './reactive-form/suggest/reactive-form-email-suggest.component'; +import { FormEmailSuggestComponent } from './form/suggest/form-email-suggest.component'; @@ -46,8 +48,10 @@ const routes: Routes = [ ], declarations: [ ReactiveFormEmailComponent, + ReactiveFormEmailSuggestComponent, FormEmailComponent, - EmailValidatorComponent + EmailValidatorComponent, + FormEmailSuggestComponent ] }) export class EmailModule { } diff --git a/examples/src/app/email-validator/form/email/form-email.component.html b/examples/src/app/email-validator/form/email/form-email.component.html index d91fc2a..25a2c6b 100644 --- a/examples/src/app/email-validator/form/email/form-email.component.html +++ b/examples/src/app/email-validator/form/email/form-email.component.html @@ -1,6 +1,6 @@
- Needs to be a number + Is not a email
\ No newline at end of file diff --git a/examples/src/app/email-validator/form/suggest/form-email-suggest.component.html b/examples/src/app/email-validator/form/suggest/form-email-suggest.component.html new file mode 100644 index 0000000..5e7e329 --- /dev/null +++ b/examples/src/app/email-validator/form/suggest/form-email-suggest.component.html @@ -0,0 +1,6 @@ +
+ + + Did you mean:{{formControl.getError('suggestion').domain}} + +
\ No newline at end of file diff --git a/examples/src/app/email-validator/form/suggest/form-email-suggest.component.ts b/examples/src/app/email-validator/form/suggest/form-email-suggest.component.ts new file mode 100644 index 0000000..70aa6b8 --- /dev/null +++ b/examples/src/app/email-validator/form/suggest/form-email-suggest.component.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +import { EmailOptions } from 'ngx-validators/components/email/email-util'; + +@Component({ + selector: 'app-email-suggest', + templateUrl: './form-email-suggest.component.html' +}) + +export class FormEmailSuggestComponent { + model: any; + + // Add your own domains via EmailOptions + customOptions: EmailOptions = { + domains: ['msn.com', 'bellsouth.net', + 'telus.net', 'comcast.net', 'optusnet.com.au', + 'earthlink.net', 'qq.com', 'sky.com', 'icloud.com', + 'mac.com', 'sympatico.ca', 'googlemail.com', + 'att.net', 'xtra.co.nz', 'web.de', 'mymail.com', + 'cox.net', 'gmail.com', 'ymail.com', 'yahoo.com', + 'aim.com', 'rogers.com', 'verizon.net', + 'rocketmail.com', 'google.com', 'optonline.net', + 'sbcglobal.net', 'aol.com', 'me.com', 'btinternet.com', + 'charter.net', 'shaw.ca'], + secondLevelDomains: ['yahoo', 'hotmail', 'mail', 'live', 'outlook', 'gmx', 'mymail'], + topLevelDomains: ['com', 'com.au', 'com.tw', 'ca', 'co.nz', 'co.uk', 'de', + 'fr', 'it', 'ru', 'net', 'org', 'edu', 'gov', 'jp', 'nl', 'kr', 'se', 'eu', + 'ie', 'co.il', 'us', 'at', 'be', 'dk', 'hk', 'es', 'gr', 'ch', 'no', 'cz', + 'in', 'net', 'net.au', 'info', 'biz', 'mil', 'co.jp', 'sg', 'hu', 'uk'] + }; + + addToForm(email) { + this.model = email; + } +} diff --git a/examples/src/app/email-validator/reactive-form/email/reactive-form-email.component.ts b/examples/src/app/email-validator/reactive-form/email/reactive-form-email.component.ts index e3cd0fc..fd15033 100644 --- a/examples/src/app/email-validator/reactive-form/email/reactive-form-email.component.ts +++ b/examples/src/app/email-validator/reactive-form/email/reactive-form-email.component.ts @@ -17,4 +17,8 @@ export class ReactiveFormEmailComponent implements OnInit { 'email': this.email, }); } + + addToForm(email) { + this.form.get('email').setValue(email); + } } diff --git a/examples/src/app/email-validator/reactive-form/suggest/reactive-form-email-suggest.component.html b/examples/src/app/email-validator/reactive-form/suggest/reactive-form-email-suggest.component.html new file mode 100644 index 0000000..a0fe759 --- /dev/null +++ b/examples/src/app/email-validator/reactive-form/suggest/reactive-form-email-suggest.component.html @@ -0,0 +1,6 @@ +
+ + + Did you mean:{{email.getError('suggestion').domain}} + +
\ No newline at end of file diff --git a/examples/src/app/email-validator/reactive-form/suggest/reactive-form-email-suggest.component.ts b/examples/src/app/email-validator/reactive-form/suggest/reactive-form-email-suggest.component.ts new file mode 100644 index 0000000..8b0a115 --- /dev/null +++ b/examples/src/app/email-validator/reactive-form/suggest/reactive-form-email-suggest.component.ts @@ -0,0 +1,43 @@ +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { EmailValidators } from 'ngx-validators'; +import { Component, OnInit } from '@angular/core'; +import { EmailOptions } from 'ngx-validators/components/email/email-util'; + +@Component({ + selector: 'app-reactive-email-suggest', + templateUrl: './reactive-form-email-suggest.component.html' +}) +export class ReactiveFormEmailSuggestComponent implements OnInit { + + // Add your own domains via EmailOptions + customOptions: EmailOptions = { + domains: ['msn.com', 'bellsouth.net', + 'telus.net', 'comcast.net', 'optusnet.com.au', + 'earthlink.net', 'qq.com', 'sky.com', 'icloud.com', + 'mac.com', 'sympatico.ca', 'googlemail.com', + 'att.net', 'xtra.co.nz', 'web.de', + 'cox.net', 'gmail.com', 'ymail.com', 'yahoo.com', + 'aim.com', 'rogers.com', 'verizon.net', + 'rocketmail.com', 'google.com', 'optonline.net', + 'sbcglobal.net', 'aol.com', 'me.com', 'btinternet.com', + 'charter.net', 'shaw.ca'], + secondLevelDomains: ['yahoo', 'hotmail', 'mail', 'live', 'outlook', 'gmx'], + topLevelDomains: ['com', 'com.au', 'com.tw', 'ca', 'co.nz', 'co.uk', 'de', + 'fr', 'it', 'ru', 'net', 'org', 'edu', 'gov', 'jp', 'nl', 'kr', 'se', 'eu', + 'ie', 'co.il', 'us', 'at', 'be', 'dk', 'hk', 'es', 'gr', 'ch', 'no', 'cz', + 'in', 'net', 'net.au', 'info', 'biz', 'mil', 'co.jp', 'sg', 'hu', 'uk'] + }; + form: FormGroup; + email = new FormControl('', Validators.compose([EmailValidators.suggest(this.customOptions)])); + constructor(protected _fb: FormBuilder) { } + + ngOnInit() { + this.form = this._fb.group({ + 'email': this.email, + }); + } + + addToForm(email) { + this.form.get('email').setValue(email); + } +} diff --git a/examples/src/app/items.ts b/examples/src/app/items.ts index 47232b8..62098e2 100644 --- a/examples/src/app/items.ts +++ b/examples/src/app/items.ts @@ -71,6 +71,14 @@ export const email: Validator[] = [ formTS: require('!raw-loader!./email-validator/form/email/form-email.component'), formHTML: require('!raw-loader!./email-validator/form/email/form-email.component.html'), hint: '(follows the HTML5 rules)', + }, + { + name: 'suggest', + reactiveformTS: require('!raw-loader!./email-validator/reactive-form/suggest/reactive-form-email-suggest.component'), + reactiveformHTML: require('!raw-loader!./email-validator/reactive-form/suggest/reactive-form-email-suggest.component.html'), + formTS: require('!raw-loader!./email-validator/form/suggest/form-email-suggest.component'), + formHTML: require('!raw-loader!./email-validator/form/suggest/form-email-suggest.component.html'), + hint: '(thanks to mailcheck)', } ]; @@ -198,10 +206,10 @@ export const creditcards: Validator[] = [ ]; export const items: Items[] = [ - {linkPrefix: '/email/', validators: email, name: 'Email'}, - {linkPrefix: '/password/', validators: password, name: 'Password'}, - {linkPrefix: '/equal-to/', validators: equalTo, name: 'Equal To'}, - {linkPrefix: '/creditcard/', validators: creditcards, name: 'Creditcards'}, - {linkPrefix: '/universal/', validators: universal, name: 'Universal'} + { linkPrefix: '/email/', validators: email, name: 'Email' }, + { linkPrefix: '/password/', validators: password, name: 'Password' }, + { linkPrefix: '/equal-to/', validators: equalTo, name: 'Equal To' }, + { linkPrefix: '/creditcard/', validators: creditcards, name: 'Creditcards' }, + { linkPrefix: '/universal/', validators: universal, name: 'Universal' } ]; diff --git a/examples/src/app/util/codeviewer/codeviewer.component.ts b/examples/src/app/util/codeviewer/codeviewer.component.ts index 4f5290f..94246de 100644 --- a/examples/src/app/util/codeviewer/codeviewer.component.ts +++ b/examples/src/app/util/codeviewer/codeviewer.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -//import * as hljs from 'highlight.js'; +// import * as hljs from 'highlight.js'; @Component({ selector: 'app-codeviewer', @@ -10,8 +10,4 @@ export class CodeviewerComponent { @Input() html: any; @Input() tsCode: any; - - constructor() { - //hljs.initHighlighting(); - } } diff --git a/examples/src/styles.css b/examples/src/styles.css index 5b7b3ad..2814b65 100644 --- a/examples/src/styles.css +++ b/examples/src/styles.css @@ -41,4 +41,9 @@ body { min-height: 100%; width: 100%; min-width: 100%; +} + +.clickable { + cursor: pointer; + text-decoration: underline; } \ No newline at end of file