-
-
Notifications
You must be signed in to change notification settings - Fork 861
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
💥 breaking(format): re-impelement formatter
- Loading branch information
Showing
2 changed files
with
169 additions
and
115 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,61 +1,117 @@ | ||
/* @flow */ | ||
|
||
import { isNull, hasOwn } from './util' | ||
import { warn, isObject } from './util' | ||
|
||
export default class BaseFormatter { | ||
_options: FormatterOptions | ||
_caches: { [key: string]: Array<Token> } | ||
|
||
constructor (options: FormatterOptions = {}) { | ||
this._options = options | ||
this._caches = Object.create(null) | ||
} | ||
|
||
get options (): FormatterOptions { return this._options } | ||
|
||
format (message: string, ...values: any): string { | ||
return template(message, ...values) | ||
format (message: string, values: any): any { | ||
let tokens: Array<Token> = this._caches[message] | ||
if (!tokens) { | ||
tokens = parse(message) | ||
this._caches[message] = tokens | ||
} | ||
return compile(tokens, values) | ||
} | ||
} | ||
|
||
/** | ||
* String format template | ||
* - Inspired: | ||
* https://github.com/Matt-Esch/string-template/index.js | ||
*/ | ||
|
||
const RE_NARGS: RegExp = /(%|)\{([0-9a-zA-Z_]+)\}/g | ||
|
||
/** | ||
* template | ||
* | ||
* @param {String} string | ||
* @param {Array} ...values | ||
* @return {String} | ||
*/ | ||
|
||
export function template (str: string, ...values: any): string { | ||
if (values.length === 1 && typeof values[0] === 'object') { | ||
values = values[0] | ||
} else { | ||
values = {} | ||
} | ||
type Token = { | ||
type: 'text' | 'named' | 'list' | 'unknown', | ||
value: string | ||
} | ||
|
||
if (!values || !values.hasOwnProperty) { | ||
values = {} | ||
} | ||
const RE_TOKEN_LIST_VALUE: RegExp = /^(\d)+/ | ||
const RE_TOKEN_NAMED_VALUE: RegExp = /^(\w)+/ | ||
|
||
return str.replace(RE_NARGS, (match, prefix, i, index) => { | ||
let result: string | ||
export function parse (format: string): Array<Token> { | ||
const tokens: Array<Token> = [] | ||
let position: number = 0 | ||
|
||
if (str[index - 1] === '{' && | ||
str[index + match.length] === '}') { | ||
return i | ||
} else { | ||
result = hasOwn(values, i) ? values[i] : match | ||
if (isNull(result)) { | ||
return '' | ||
let text: string = '' | ||
while (position < format.length) { | ||
let char: string = format[position++] | ||
if (char === '{') { | ||
if (text) { | ||
tokens.push({ type: 'text', value: text }) | ||
} | ||
|
||
return result | ||
text = '' | ||
let sub: string = '' | ||
char = format[position++] | ||
while (char !== '}') { | ||
sub += char | ||
char = format[position++] | ||
} | ||
|
||
const type = RE_TOKEN_LIST_VALUE.test(sub) | ||
? 'list' | ||
: RE_TOKEN_NAMED_VALUE.test(sub) | ||
? 'named' | ||
: 'unknown' | ||
tokens.push({ value: sub, type }) | ||
} else if (char === '%') { | ||
// when found rails i18n syntax, skip text capture | ||
} else { | ||
text += char | ||
} | ||
} | ||
|
||
text && tokens.push({ type: 'text', value: text }) | ||
|
||
return tokens | ||
} | ||
|
||
export function compile (tokens: Array<Token>, values: Object | Array<any>): Array<any> { | ||
const compiled: Array<any> = [] | ||
let index: number = 0 | ||
|
||
const mode: string = Array.isArray(values) | ||
? 'list' | ||
: isObject(values) | ||
? 'named' | ||
: 'unknown' | ||
if (mode === 'unknown') { return compiled } | ||
|
||
while (index < tokens.length) { | ||
const token: Token = tokens[index] | ||
switch (token.type) { | ||
case 'text': | ||
compiled.push(token.value) | ||
break | ||
case 'list': | ||
if (mode === 'list') { | ||
compiled.push(values[parseInt(token.value, 10)]) | ||
} else { | ||
if (process.env.NODE_ENV !== 'production') { | ||
warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`) | ||
} | ||
} | ||
break | ||
case 'named': | ||
if (mode === 'named') { | ||
compiled.push((values: any)[token.value]) | ||
} else { | ||
if (process.env.NODE_ENV !== 'production') { | ||
warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`) | ||
} | ||
} | ||
break | ||
case 'unknown': | ||
if (process.env.NODE_ENV !== 'production') { | ||
warn(`Detect 'unknown' type of token!`) | ||
} | ||
break | ||
} | ||
}) | ||
index++ | ||
} | ||
|
||
return compiled | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,91 +1,89 @@ | ||
import { template as format } from '../../src/format' | ||
import { template as format, parse, compile } from '../../src/format' | ||
|
||
describe('format', () => { | ||
describe('argument', () => { | ||
describe('Object', () => { | ||
describe('default delimiter', () => { | ||
it('should be replace with object value', () => { | ||
const template = 'name: {name}, email: {email}' | ||
assert.equal(format(template, { | ||
name: 'kazupon', email: '[email protected]' | ||
}), 'name: kazupon, email: [email protected]') | ||
}) | ||
}) | ||
|
||
describe('RoR delimiter', () => { | ||
it('should be replace with object value', () => { | ||
const template = 'name: %{name}, email: %{email}' | ||
assert.equal(format(template, { | ||
name: 'kazupon', email: '[email protected]' | ||
}), 'name: kazupon, email: [email protected]') | ||
}) | ||
}) | ||
|
||
describe('missing', () => { | ||
it('should be replace with as is', () => { | ||
const template = 'name: {name}, email: {email}' | ||
assert.equal(format(template, { | ||
name: 'kazupon' | ||
}), 'name: kazupon, email: {email}') | ||
}) | ||
}) | ||
}) | ||
|
||
describe('Array', () => { | ||
it('should be replace with array value', () => { | ||
const template = 'name: {0}, email: {1}' | ||
assert.equal( | ||
format(template, ['kazupon', '[email protected]']), | ||
'name: kazupon, email: [email protected]' | ||
) | ||
}) | ||
}) | ||
|
||
describe('null', () => { | ||
it('should be replace with empty', () => { | ||
const template = 'name: {0}, email: {1}' | ||
assert.equal(format(template, null), 'name: {0}, email: {1}') | ||
}) | ||
describe('parse', () => { | ||
describe('list', () => { | ||
it('should be parsed', () => { | ||
const tokens = parse('name: {0}, email: {1}') | ||
assert(tokens.length === 4) | ||
assert.equal(tokens[0].type, 'text') | ||
assert.equal(tokens[0].value, 'name: ') | ||
assert.equal(tokens[1].type, 'list') | ||
assert.equal(tokens[1].value, '0') | ||
assert.equal(tokens[2].type, 'text') | ||
assert.equal(tokens[2].value, ', email: ') | ||
assert.equal(tokens[3].type, 'list') | ||
assert.equal(tokens[3].value, '1') | ||
}) | ||
}) | ||
|
||
describe('undefined', () => { | ||
it('should be replace with empty', () => { | ||
const template = 'name: {0}, email: {1}' | ||
assert.equal(format(template, undefined), 'name: {0}, email: {1}') | ||
}) | ||
describe('named', () => { | ||
it('should be parsed', () => { | ||
const tokens = parse('name: {name}, email: {email}') | ||
assert(tokens.length === 4) | ||
assert.equal(tokens[0].type, 'text') | ||
assert.equal(tokens[0].value, 'name: ') | ||
assert.equal(tokens[1].type, 'named') | ||
assert.equal(tokens[1].value, 'name') | ||
assert.equal(tokens[2].type, 'text') | ||
assert.equal(tokens[2].value, ', email: ') | ||
assert.equal(tokens[3].type, 'named') | ||
assert.equal(tokens[3].value, 'email') | ||
}) | ||
}) | ||
|
||
describe('not specify', () => { | ||
it('should be replace with empty', () => { | ||
const template = 'name: {0}, email: {1}' | ||
assert.equal(format(template), 'name: {0}, email: {1}') | ||
}) | ||
describe('rails i18n format syntax', () => { | ||
it('should be parsed', () => { | ||
const tokens = parse('name: %{name}, email: %{email}') | ||
assert(tokens.length === 4) | ||
assert.equal(tokens[0].type, 'text') | ||
assert.equal(tokens[0].value, 'name: ') | ||
assert.equal(tokens[1].type, 'named') | ||
assert.equal(tokens[1].value, 'name') | ||
assert.equal(tokens[2].type, 'text') | ||
assert.equal(tokens[2].value, ', email: ') | ||
assert.equal(tokens[3].type, 'named') | ||
assert.equal(tokens[3].value, 'email') | ||
}) | ||
}) | ||
|
||
|
||
describe('argument data', () => { | ||
describe('primivive', () => { | ||
it('should be replace with primivive value', () => { | ||
const template = 'a: {0}, b: {1}' | ||
assert.equal(format(template, [1, 2]), 'a: 1, b: 2') | ||
}) | ||
describe('not support format', () => { | ||
it('should be parsed', () => { | ||
const tokens = parse('name: { name1}, email: {%email}') | ||
assert(tokens.length === 4) | ||
assert.equal(tokens[0].type, 'text') | ||
assert.equal(tokens[0].value, 'name: ') | ||
assert.equal(tokens[1].type, 'unknown') | ||
assert.equal(tokens[1].value, ' name1') | ||
assert.equal(tokens[2].type, 'text') | ||
assert.equal(tokens[2].value, ', email: ') | ||
assert.equal(tokens[3].type, 'unknown') | ||
assert.equal(tokens[3].value, '%email') | ||
}) | ||
}) | ||
}) | ||
|
||
describe('null', () => { | ||
it('should be replace with empty string', () => { | ||
const template = 'name: {0}, email: {1}' | ||
assert.equal(format(template, [null, null]), 'name: , email: ') | ||
}) | ||
describe('compile', () => { | ||
describe('list token', () => { | ||
it('should be compiled', () => { | ||
const tokens = parse('name: {0}, age: {1}') | ||
const compiled = compile(tokens, ['kazupon', '0x20']) | ||
assert(compiled.length === 4) | ||
assert.equal(compiled[0], 'name: ') | ||
assert.equal(compiled[1], 'kazupon') | ||
assert.equal(compiled[2], ', age: ') | ||
assert.equal(compiled[3], '0x20') | ||
}) | ||
}) | ||
|
||
describe('undefined', () => { | ||
it('should be replace with empty string', () => { | ||
const template = 'name: {name}, email: {email}' | ||
assert.equal(format(template, { | ||
name: undefined, email: undefined | ||
}), 'name: , email: ') | ||
}) | ||
describe('named token', () => { | ||
it('should be compiled', () => { | ||
const tokens = parse('name: {name}, age: {age}') | ||
const compiled = compile(tokens, { name: 'kazupon', age: '0x20' }) | ||
assert(compiled.length === 4) | ||
assert.equal(compiled[0], 'name: ') | ||
assert.equal(compiled[1], 'kazupon') | ||
assert.equal(compiled[2], ', age: ') | ||
assert.equal(compiled[3], '0x20') | ||
}) | ||
}) | ||
}) |