Skip to content

Commit

Permalink
💥 breaking(format): re-impelement formatter
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed May 2, 2017
1 parent 059034f commit a8c046d
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 115 deletions.
134 changes: 95 additions & 39 deletions src/format.js
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
}
150 changes: 74 additions & 76 deletions test/unit/format.test.js
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')
})
})
})

0 comments on commit a8c046d

Please sign in to comment.