| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- // @flow
- 'use strict'
- /*::
- export type AST = Element[]
- export type Element = string | Placeholder
- export type Placeholder = Plural | Styled | Typed | Simple
- export type Plural = [ string, 'plural' | 'selectordinal', number, SubMessages ]
- export type Styled = [ string, string, string | SubMessages ]
- export type Typed = [ string, string ]
- export type Simple = [ string ]
- export type SubMessages = { [string]: AST }
- export type Token = [ TokenType, string ]
- export type TokenType = 'text' | 'space' | 'id' | 'type' | 'style' | 'offset' | 'number' | 'selector' | 'syntax'
- type Context = {|
- pattern: string,
- index: number,
- tagsType: ?string,
- tokens: ?Token[]
- |}
- */
- var ARG_OPN = '{'
- var ARG_CLS = '}'
- var ARG_SEP = ','
- var NUM_ARG = '#'
- var TAG_OPN = '<'
- var TAG_CLS = '>'
- var TAG_END = '</'
- var TAG_SELF_CLS = '/>'
- var ESC = '\''
- var OFFSET = 'offset:'
- var simpleTypes = [
- 'number',
- 'date',
- 'time',
- 'ordinal',
- 'duration',
- 'spellout'
- ]
- var submTypes = [
- 'plural',
- 'select',
- 'selectordinal'
- ]
- /**
- * parse
- *
- * Turns this:
- * `You have { numBananas, plural,
- * =0 {no bananas}
- * one {a banana}
- * other {# bananas}
- * } for sale`
- *
- * into this:
- * [ "You have ", [ "numBananas", "plural", 0, {
- * "=0": [ "no bananas" ],
- * "one": [ "a banana" ],
- * "other": [ [ '#' ], " bananas" ]
- * } ], " for sale." ]
- *
- * tokens:
- * [
- * [ "text", "You have " ],
- * [ "syntax", "{" ],
- * [ "space", " " ],
- * [ "id", "numBananas" ],
- * [ "syntax", ", " ],
- * [ "space", " " ],
- * [ "type", "plural" ],
- * [ "syntax", "," ],
- * [ "space", "\n " ],
- * [ "selector", "=0" ],
- * [ "space", " " ],
- * [ "syntax", "{" ],
- * [ "text", "no bananas" ],
- * [ "syntax", "}" ],
- * [ "space", "\n " ],
- * [ "selector", "one" ],
- * [ "space", " " ],
- * [ "syntax", "{" ],
- * [ "text", "a banana" ],
- * [ "syntax", "}" ],
- * [ "space", "\n " ],
- * [ "selector", "other" ],
- * [ "space", " " ],
- * [ "syntax", "{" ],
- * [ "syntax", "#" ],
- * [ "text", " bananas" ],
- * [ "syntax", "}" ],
- * [ "space", "\n" ],
- * [ "syntax", "}" ],
- * [ "text", " for sale." ]
- * ]
- **/
- exports = module.exports = function parse (
- pattern/*: string */,
- options/*:: ?: { tagsType?: string, tokens?: Token[] } */
- )/*: AST */ {
- return parseAST({
- pattern: String(pattern),
- index: 0,
- tagsType: (options && options.tagsType) || null,
- tokens: (options && options.tokens) || null
- }, '')
- }
- function parseAST (current/*: Context */, parentType/*: string */)/*: AST */ {
- var pattern = current.pattern
- var length = pattern.length
- var elements/*: AST */ = []
- var start = current.index
- var text = parseText(current, parentType)
- if (text) elements.push(text)
- if (text && current.tokens) current.tokens.push(['text', pattern.slice(start, current.index)])
- while (current.index < length) {
- if (pattern[current.index] === ARG_CLS) {
- if (!parentType) throw expected(current)
- break
- }
- if (parentType && current.tagsType && pattern.slice(current.index, current.index + TAG_END.length) === TAG_END) break
- elements.push(parsePlaceholder(current))
- start = current.index
- text = parseText(current, parentType)
- if (text) elements.push(text)
- if (text && current.tokens) current.tokens.push(['text', pattern.slice(start, current.index)])
- }
- return elements
- }
- function parseText (current/*: Context */, parentType/*: string */)/*: string */ {
- var pattern = current.pattern
- var length = pattern.length
- var isHashSpecial = (parentType === 'plural' || parentType === 'selectordinal')
- var isAngleSpecial = !!current.tagsType
- var isArgStyle = (parentType === '{style}')
- var text = ''
- while (current.index < length) {
- var char = pattern[current.index]
- if (
- char === ARG_OPN || char === ARG_CLS ||
- (isHashSpecial && char === NUM_ARG) ||
- (isAngleSpecial && char === TAG_OPN) ||
- (isArgStyle && isWhitespace(char.charCodeAt(0)))
- ) {
- break
- } else if (char === ESC) {
- char = pattern[++current.index]
- if (char === ESC) { // double is always 1 '
- text += char
- ++current.index
- } else if (
- // only when necessary
- char === ARG_OPN || char === ARG_CLS ||
- (isHashSpecial && char === NUM_ARG) ||
- (isAngleSpecial && char === TAG_OPN) ||
- isArgStyle
- ) {
- text += char
- while (++current.index < length) {
- char = pattern[current.index]
- if (char === ESC && pattern[current.index + 1] === ESC) { // double is always 1 '
- text += ESC
- ++current.index
- } else if (char === ESC) { // end of quoted
- ++current.index
- break
- } else {
- text += char
- }
- }
- } else { // lone ' is just a '
- text += ESC
- // already incremented
- }
- } else {
- text += char
- ++current.index
- }
- }
- return text
- }
- function isWhitespace (code/*: number */)/*: boolean */ {
- return (
- (code >= 0x09 && code <= 0x0D) ||
- code === 0x20 || code === 0x85 || code === 0xA0 || code === 0x180E ||
- (code >= 0x2000 && code <= 0x200D) ||
- code === 0x2028 || code === 0x2029 || code === 0x202F || code === 0x205F ||
- code === 0x2060 || code === 0x3000 || code === 0xFEFF
- )
- }
- function skipWhitespace (current/*: Context */)/*: void */ {
- var pattern = current.pattern
- var length = pattern.length
- var start = current.index
- while (current.index < length && isWhitespace(pattern.charCodeAt(current.index))) {
- ++current.index
- }
- if (start < current.index && current.tokens) {
- current.tokens.push(['space', current.pattern.slice(start, current.index)])
- }
- }
- function parsePlaceholder (current/*: Context */)/*: Placeholder */ {
- var pattern = current.pattern
- if (pattern[current.index] === NUM_ARG) {
- if (current.tokens) current.tokens.push(['syntax', NUM_ARG])
- ++current.index // move passed #
- return [NUM_ARG]
- }
- var tag = parseTag(current)
- if (tag) return tag
- /* istanbul ignore if should be unreachable if parseAST and parseText are right */
- if (pattern[current.index] !== ARG_OPN) throw expected(current, ARG_OPN)
- if (current.tokens) current.tokens.push(['syntax', ARG_OPN])
- ++current.index // move passed {
- skipWhitespace(current)
- var id = parseId(current)
- if (!id) throw expected(current, 'placeholder id')
- if (current.tokens) current.tokens.push(['id', id])
- skipWhitespace(current)
- var char = pattern[current.index]
- if (char === ARG_CLS) { // end placeholder
- if (current.tokens) current.tokens.push(['syntax', ARG_CLS])
- ++current.index // move passed }
- return [id]
- }
- if (char !== ARG_SEP) throw expected(current, ARG_SEP + ' or ' + ARG_CLS)
- if (current.tokens) current.tokens.push(['syntax', ARG_SEP])
- ++current.index // move passed ,
- skipWhitespace(current)
- var type = parseId(current)
- if (!type) throw expected(current, 'placeholder type')
- if (current.tokens) current.tokens.push(['type', type])
- skipWhitespace(current)
- char = pattern[current.index]
- if (char === ARG_CLS) { // end placeholder
- if (current.tokens) current.tokens.push(['syntax', ARG_CLS])
- if (type === 'plural' || type === 'selectordinal' || type === 'select') {
- throw expected(current, type + ' sub-messages')
- }
- ++current.index // move passed }
- return [id, type]
- }
- if (char !== ARG_SEP) throw expected(current, ARG_SEP + ' or ' + ARG_CLS)
- if (current.tokens) current.tokens.push(['syntax', ARG_SEP])
- ++current.index // move passed ,
- skipWhitespace(current)
- var arg
- if (type === 'plural' || type === 'selectordinal') {
- var offset = parsePluralOffset(current)
- skipWhitespace(current)
- arg = [id, type, offset, parseSubMessages(current, type)]
- } else if (type === 'select') {
- arg = [id, type, parseSubMessages(current, type)]
- } else if (simpleTypes.indexOf(type) >= 0) {
- arg = [id, type, parseSimpleFormat(current)]
- } else { // custom placeholder type
- var index = current.index
- var format/*: string | SubMessages */ = parseSimpleFormat(current)
- skipWhitespace(current)
- if (pattern[current.index] === ARG_OPN) {
- current.index = index // rewind, since should have been submessages
- format = parseSubMessages(current, type)
- }
- arg = [id, type, format]
- }
- skipWhitespace(current)
- if (pattern[current.index] !== ARG_CLS) throw expected(current, ARG_CLS)
- if (current.tokens) current.tokens.push(['syntax', ARG_CLS])
- ++current.index // move passed }
- return arg
- }
- function parseTag (current/*: Context */)/*: ?Placeholder */ {
- var tagsType = current.tagsType
- if (!tagsType || current.pattern[current.index] !== TAG_OPN) return
- if (current.pattern.slice(current.index, current.index + TAG_END.length) === TAG_END) {
- throw expected(current, null, 'closing tag without matching opening tag')
- }
- if (current.tokens) current.tokens.push(['syntax', TAG_OPN])
- ++current.index // move passed <
- var id = parseId(current, true)
- if (!id) throw expected(current, 'placeholder id')
- if (current.tokens) current.tokens.push(['id', id])
- skipWhitespace(current)
- if (current.pattern.slice(current.index, current.index + TAG_SELF_CLS.length) === TAG_SELF_CLS) {
- if (current.tokens) current.tokens.push(['syntax', TAG_SELF_CLS])
- current.index += TAG_SELF_CLS.length
- return [id, tagsType]
- }
- if (current.pattern[current.index] !== TAG_CLS) throw expected(current, TAG_CLS)
- if (current.tokens) current.tokens.push(['syntax', TAG_CLS])
- ++current.index // move passed >
- var children = parseAST(current, tagsType)
- var end = current.index
- if (current.pattern.slice(current.index, current.index + TAG_END.length) !== TAG_END) throw expected(current, TAG_END + id + TAG_CLS)
- if (current.tokens) current.tokens.push(['syntax', TAG_END])
- current.index += TAG_END.length
- var closeId = parseId(current, true)
- if (closeId && current.tokens) current.tokens.push(['id', closeId])
- if (id !== closeId) {
- current.index = end // rewind for better error message
- throw expected(current, TAG_END + id + TAG_CLS, TAG_END + closeId + TAG_CLS)
- }
- skipWhitespace(current)
- if (current.pattern[current.index] !== TAG_CLS) throw expected(current, TAG_CLS)
- if (current.tokens) current.tokens.push(['syntax', TAG_CLS])
- ++current.index // move passed >
- return [id, tagsType, { children: children }]
- }
- function parseId (current/*: Context */, isTag/*:: ?: boolean */)/*: string */ {
- var pattern = current.pattern
- var length = pattern.length
- var id = ''
- while (current.index < length) {
- var char = pattern[current.index]
- if (
- char === ARG_OPN || char === ARG_CLS || char === ARG_SEP ||
- char === NUM_ARG || char === ESC || isWhitespace(char.charCodeAt(0)) ||
- (isTag && (char === TAG_OPN || char === TAG_CLS || char === '/'))
- ) break
- id += char
- ++current.index
- }
- return id
- }
- function parseSimpleFormat (current/*: Context */)/*: string */ {
- var start = current.index
- var style = parseText(current, '{style}')
- if (!style) throw expected(current, 'placeholder style name')
- if (current.tokens) current.tokens.push(['style', current.pattern.slice(start, current.index)])
- return style
- }
- function parsePluralOffset (current/*: Context */)/*: number */ {
- var pattern = current.pattern
- var length = pattern.length
- var offset = 0
- if (pattern.slice(current.index, current.index + OFFSET.length) === OFFSET) {
- if (current.tokens) current.tokens.push(['offset', 'offset'], ['syntax', ':'])
- current.index += OFFSET.length // move passed offset:
- skipWhitespace(current)
- var start = current.index
- while (current.index < length && isDigit(pattern.charCodeAt(current.index))) {
- ++current.index
- }
- if (start === current.index) throw expected(current, 'offset number')
- if (current.tokens) current.tokens.push(['number', pattern.slice(start, current.index)])
- offset = +pattern.slice(start, current.index)
- }
- return offset
- }
- function isDigit (code/*: number */)/*: boolean */ {
- return (code >= 0x30 && code <= 0x39)
- }
- function parseSubMessages (current/*: Context */, parentType/*: string */)/*: SubMessages */ {
- var pattern = current.pattern
- var length = pattern.length
- var options/*: SubMessages */ = {}
- while (current.index < length && pattern[current.index] !== ARG_CLS) {
- var selector = parseId(current)
- if (!selector) throw expected(current, 'sub-message selector')
- if (current.tokens) current.tokens.push(['selector', selector])
- skipWhitespace(current)
- options[selector] = parseSubMessage(current, parentType)
- skipWhitespace(current)
- }
- if (!options.other && submTypes.indexOf(parentType) >= 0) {
- throw expected(current, null, null, '"other" sub-message must be specified in ' + parentType)
- }
- return options
- }
- function parseSubMessage (current/*: Context */, parentType/*: string */)/*: AST */ {
- if (current.pattern[current.index] !== ARG_OPN) throw expected(current, ARG_OPN + ' to start sub-message')
- if (current.tokens) current.tokens.push(['syntax', ARG_OPN])
- ++current.index // move passed {
- var message = parseAST(current, parentType)
- if (current.pattern[current.index] !== ARG_CLS) throw expected(current, ARG_CLS + ' to end sub-message')
- if (current.tokens) current.tokens.push(['syntax', ARG_CLS])
- ++current.index // move passed }
- return message
- }
- function expected (current/*: Context */, expected/*:: ?: ?string */, found/*:: ?: ?string */, message/*:: ?: string */) {
- var pattern = current.pattern
- var lines = pattern.slice(0, current.index).split(/\r?\n/)
- var offset = current.index
- var line = lines.length
- var column = lines.slice(-1)[0].length
- found = found || (
- (current.index >= pattern.length) ? 'end of message pattern'
- : (parseId(current) || pattern[current.index])
- )
- if (!message) message = errorMessage(expected, found)
- message += ' in ' + pattern.replace(/\r?\n/g, '\n')
- return new SyntaxError(message, expected, found, offset, line, column)
- }
- function errorMessage (expected/*: ?string */, found/* string */) {
- if (!expected) return 'Unexpected ' + found + ' found'
- return 'Expected ' + expected + ' but found ' + found
- }
- /**
- * SyntaxError
- * Holds information about bad syntax found in a message pattern
- **/
- function SyntaxError (message/*: string */, expected/*: ?string */, found/*: ?string */, offset/*: number */, line/*: number */, column/*: number */) {
- Error.call(this, message)
- this.name = 'SyntaxError'
- this.message = message
- this.expected = expected
- this.found = found
- this.offset = offset
- this.line = line
- this.column = column
- }
- SyntaxError.prototype = Object.create(Error.prototype)
- exports.SyntaxError = SyntaxError
|