index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. // @flow
  2. 'use strict'
  3. /*::
  4. export type AST = Element[]
  5. export type Element = string | Placeholder
  6. export type Placeholder = Plural | Styled | Typed | Simple
  7. export type Plural = [ string, 'plural' | 'selectordinal', number, SubMessages ]
  8. export type Styled = [ string, string, string | SubMessages ]
  9. export type Typed = [ string, string ]
  10. export type Simple = [ string ]
  11. export type SubMessages = { [string]: AST }
  12. export type Token = [ TokenType, string ]
  13. export type TokenType = 'text' | 'space' | 'id' | 'type' | 'style' | 'offset' | 'number' | 'selector' | 'syntax'
  14. type Context = {|
  15. pattern: string,
  16. index: number,
  17. tagsType: ?string,
  18. tokens: ?Token[]
  19. |}
  20. */
  21. var ARG_OPN = '{'
  22. var ARG_CLS = '}'
  23. var ARG_SEP = ','
  24. var NUM_ARG = '#'
  25. var TAG_OPN = '<'
  26. var TAG_CLS = '>'
  27. var TAG_END = '</'
  28. var TAG_SELF_CLS = '/>'
  29. var ESC = '\''
  30. var OFFSET = 'offset:'
  31. var simpleTypes = [
  32. 'number',
  33. 'date',
  34. 'time',
  35. 'ordinal',
  36. 'duration',
  37. 'spellout'
  38. ]
  39. var submTypes = [
  40. 'plural',
  41. 'select',
  42. 'selectordinal'
  43. ]
  44. /**
  45. * parse
  46. *
  47. * Turns this:
  48. * `You have { numBananas, plural,
  49. * =0 {no bananas}
  50. * one {a banana}
  51. * other {# bananas}
  52. * } for sale`
  53. *
  54. * into this:
  55. * [ "You have ", [ "numBananas", "plural", 0, {
  56. * "=0": [ "no bananas" ],
  57. * "one": [ "a banana" ],
  58. * "other": [ [ '#' ], " bananas" ]
  59. * } ], " for sale." ]
  60. *
  61. * tokens:
  62. * [
  63. * [ "text", "You have " ],
  64. * [ "syntax", "{" ],
  65. * [ "space", " " ],
  66. * [ "id", "numBananas" ],
  67. * [ "syntax", ", " ],
  68. * [ "space", " " ],
  69. * [ "type", "plural" ],
  70. * [ "syntax", "," ],
  71. * [ "space", "\n " ],
  72. * [ "selector", "=0" ],
  73. * [ "space", " " ],
  74. * [ "syntax", "{" ],
  75. * [ "text", "no bananas" ],
  76. * [ "syntax", "}" ],
  77. * [ "space", "\n " ],
  78. * [ "selector", "one" ],
  79. * [ "space", " " ],
  80. * [ "syntax", "{" ],
  81. * [ "text", "a banana" ],
  82. * [ "syntax", "}" ],
  83. * [ "space", "\n " ],
  84. * [ "selector", "other" ],
  85. * [ "space", " " ],
  86. * [ "syntax", "{" ],
  87. * [ "syntax", "#" ],
  88. * [ "text", " bananas" ],
  89. * [ "syntax", "}" ],
  90. * [ "space", "\n" ],
  91. * [ "syntax", "}" ],
  92. * [ "text", " for sale." ]
  93. * ]
  94. **/
  95. exports = module.exports = function parse (
  96. pattern/*: string */,
  97. options/*:: ?: { tagsType?: string, tokens?: Token[] } */
  98. )/*: AST */ {
  99. return parseAST({
  100. pattern: String(pattern),
  101. index: 0,
  102. tagsType: (options && options.tagsType) || null,
  103. tokens: (options && options.tokens) || null
  104. }, '')
  105. }
  106. function parseAST (current/*: Context */, parentType/*: string */)/*: AST */ {
  107. var pattern = current.pattern
  108. var length = pattern.length
  109. var elements/*: AST */ = []
  110. var start = current.index
  111. var text = parseText(current, parentType)
  112. if (text) elements.push(text)
  113. if (text && current.tokens) current.tokens.push(['text', pattern.slice(start, current.index)])
  114. while (current.index < length) {
  115. if (pattern[current.index] === ARG_CLS) {
  116. if (!parentType) throw expected(current)
  117. break
  118. }
  119. if (parentType && current.tagsType && pattern.slice(current.index, current.index + TAG_END.length) === TAG_END) break
  120. elements.push(parsePlaceholder(current))
  121. start = current.index
  122. text = parseText(current, parentType)
  123. if (text) elements.push(text)
  124. if (text && current.tokens) current.tokens.push(['text', pattern.slice(start, current.index)])
  125. }
  126. return elements
  127. }
  128. function parseText (current/*: Context */, parentType/*: string */)/*: string */ {
  129. var pattern = current.pattern
  130. var length = pattern.length
  131. var isHashSpecial = (parentType === 'plural' || parentType === 'selectordinal')
  132. var isAngleSpecial = !!current.tagsType
  133. var isArgStyle = (parentType === '{style}')
  134. var text = ''
  135. while (current.index < length) {
  136. var char = pattern[current.index]
  137. if (
  138. char === ARG_OPN || char === ARG_CLS ||
  139. (isHashSpecial && char === NUM_ARG) ||
  140. (isAngleSpecial && char === TAG_OPN) ||
  141. (isArgStyle && isWhitespace(char.charCodeAt(0)))
  142. ) {
  143. break
  144. } else if (char === ESC) {
  145. char = pattern[++current.index]
  146. if (char === ESC) { // double is always 1 '
  147. text += char
  148. ++current.index
  149. } else if (
  150. // only when necessary
  151. char === ARG_OPN || char === ARG_CLS ||
  152. (isHashSpecial && char === NUM_ARG) ||
  153. (isAngleSpecial && char === TAG_OPN) ||
  154. isArgStyle
  155. ) {
  156. text += char
  157. while (++current.index < length) {
  158. char = pattern[current.index]
  159. if (char === ESC && pattern[current.index + 1] === ESC) { // double is always 1 '
  160. text += ESC
  161. ++current.index
  162. } else if (char === ESC) { // end of quoted
  163. ++current.index
  164. break
  165. } else {
  166. text += char
  167. }
  168. }
  169. } else { // lone ' is just a '
  170. text += ESC
  171. // already incremented
  172. }
  173. } else {
  174. text += char
  175. ++current.index
  176. }
  177. }
  178. return text
  179. }
  180. function isWhitespace (code/*: number */)/*: boolean */ {
  181. return (
  182. (code >= 0x09 && code <= 0x0D) ||
  183. code === 0x20 || code === 0x85 || code === 0xA0 || code === 0x180E ||
  184. (code >= 0x2000 && code <= 0x200D) ||
  185. code === 0x2028 || code === 0x2029 || code === 0x202F || code === 0x205F ||
  186. code === 0x2060 || code === 0x3000 || code === 0xFEFF
  187. )
  188. }
  189. function skipWhitespace (current/*: Context */)/*: void */ {
  190. var pattern = current.pattern
  191. var length = pattern.length
  192. var start = current.index
  193. while (current.index < length && isWhitespace(pattern.charCodeAt(current.index))) {
  194. ++current.index
  195. }
  196. if (start < current.index && current.tokens) {
  197. current.tokens.push(['space', current.pattern.slice(start, current.index)])
  198. }
  199. }
  200. function parsePlaceholder (current/*: Context */)/*: Placeholder */ {
  201. var pattern = current.pattern
  202. if (pattern[current.index] === NUM_ARG) {
  203. if (current.tokens) current.tokens.push(['syntax', NUM_ARG])
  204. ++current.index // move passed #
  205. return [NUM_ARG]
  206. }
  207. var tag = parseTag(current)
  208. if (tag) return tag
  209. /* istanbul ignore if should be unreachable if parseAST and parseText are right */
  210. if (pattern[current.index] !== ARG_OPN) throw expected(current, ARG_OPN)
  211. if (current.tokens) current.tokens.push(['syntax', ARG_OPN])
  212. ++current.index // move passed {
  213. skipWhitespace(current)
  214. var id = parseId(current)
  215. if (!id) throw expected(current, 'placeholder id')
  216. if (current.tokens) current.tokens.push(['id', id])
  217. skipWhitespace(current)
  218. var char = pattern[current.index]
  219. if (char === ARG_CLS) { // end placeholder
  220. if (current.tokens) current.tokens.push(['syntax', ARG_CLS])
  221. ++current.index // move passed }
  222. return [id]
  223. }
  224. if (char !== ARG_SEP) throw expected(current, ARG_SEP + ' or ' + ARG_CLS)
  225. if (current.tokens) current.tokens.push(['syntax', ARG_SEP])
  226. ++current.index // move passed ,
  227. skipWhitespace(current)
  228. var type = parseId(current)
  229. if (!type) throw expected(current, 'placeholder type')
  230. if (current.tokens) current.tokens.push(['type', type])
  231. skipWhitespace(current)
  232. char = pattern[current.index]
  233. if (char === ARG_CLS) { // end placeholder
  234. if (current.tokens) current.tokens.push(['syntax', ARG_CLS])
  235. if (type === 'plural' || type === 'selectordinal' || type === 'select') {
  236. throw expected(current, type + ' sub-messages')
  237. }
  238. ++current.index // move passed }
  239. return [id, type]
  240. }
  241. if (char !== ARG_SEP) throw expected(current, ARG_SEP + ' or ' + ARG_CLS)
  242. if (current.tokens) current.tokens.push(['syntax', ARG_SEP])
  243. ++current.index // move passed ,
  244. skipWhitespace(current)
  245. var arg
  246. if (type === 'plural' || type === 'selectordinal') {
  247. var offset = parsePluralOffset(current)
  248. skipWhitespace(current)
  249. arg = [id, type, offset, parseSubMessages(current, type)]
  250. } else if (type === 'select') {
  251. arg = [id, type, parseSubMessages(current, type)]
  252. } else if (simpleTypes.indexOf(type) >= 0) {
  253. arg = [id, type, parseSimpleFormat(current)]
  254. } else { // custom placeholder type
  255. var index = current.index
  256. var format/*: string | SubMessages */ = parseSimpleFormat(current)
  257. skipWhitespace(current)
  258. if (pattern[current.index] === ARG_OPN) {
  259. current.index = index // rewind, since should have been submessages
  260. format = parseSubMessages(current, type)
  261. }
  262. arg = [id, type, format]
  263. }
  264. skipWhitespace(current)
  265. if (pattern[current.index] !== ARG_CLS) throw expected(current, ARG_CLS)
  266. if (current.tokens) current.tokens.push(['syntax', ARG_CLS])
  267. ++current.index // move passed }
  268. return arg
  269. }
  270. function parseTag (current/*: Context */)/*: ?Placeholder */ {
  271. var tagsType = current.tagsType
  272. if (!tagsType || current.pattern[current.index] !== TAG_OPN) return
  273. if (current.pattern.slice(current.index, current.index + TAG_END.length) === TAG_END) {
  274. throw expected(current, null, 'closing tag without matching opening tag')
  275. }
  276. if (current.tokens) current.tokens.push(['syntax', TAG_OPN])
  277. ++current.index // move passed <
  278. var id = parseId(current, true)
  279. if (!id) throw expected(current, 'placeholder id')
  280. if (current.tokens) current.tokens.push(['id', id])
  281. skipWhitespace(current)
  282. if (current.pattern.slice(current.index, current.index + TAG_SELF_CLS.length) === TAG_SELF_CLS) {
  283. if (current.tokens) current.tokens.push(['syntax', TAG_SELF_CLS])
  284. current.index += TAG_SELF_CLS.length
  285. return [id, tagsType]
  286. }
  287. if (current.pattern[current.index] !== TAG_CLS) throw expected(current, TAG_CLS)
  288. if (current.tokens) current.tokens.push(['syntax', TAG_CLS])
  289. ++current.index // move passed >
  290. var children = parseAST(current, tagsType)
  291. var end = current.index
  292. if (current.pattern.slice(current.index, current.index + TAG_END.length) !== TAG_END) throw expected(current, TAG_END + id + TAG_CLS)
  293. if (current.tokens) current.tokens.push(['syntax', TAG_END])
  294. current.index += TAG_END.length
  295. var closeId = parseId(current, true)
  296. if (closeId && current.tokens) current.tokens.push(['id', closeId])
  297. if (id !== closeId) {
  298. current.index = end // rewind for better error message
  299. throw expected(current, TAG_END + id + TAG_CLS, TAG_END + closeId + TAG_CLS)
  300. }
  301. skipWhitespace(current)
  302. if (current.pattern[current.index] !== TAG_CLS) throw expected(current, TAG_CLS)
  303. if (current.tokens) current.tokens.push(['syntax', TAG_CLS])
  304. ++current.index // move passed >
  305. return [id, tagsType, { children: children }]
  306. }
  307. function parseId (current/*: Context */, isTag/*:: ?: boolean */)/*: string */ {
  308. var pattern = current.pattern
  309. var length = pattern.length
  310. var id = ''
  311. while (current.index < length) {
  312. var char = pattern[current.index]
  313. if (
  314. char === ARG_OPN || char === ARG_CLS || char === ARG_SEP ||
  315. char === NUM_ARG || char === ESC || isWhitespace(char.charCodeAt(0)) ||
  316. (isTag && (char === TAG_OPN || char === TAG_CLS || char === '/'))
  317. ) break
  318. id += char
  319. ++current.index
  320. }
  321. return id
  322. }
  323. function parseSimpleFormat (current/*: Context */)/*: string */ {
  324. var start = current.index
  325. var style = parseText(current, '{style}')
  326. if (!style) throw expected(current, 'placeholder style name')
  327. if (current.tokens) current.tokens.push(['style', current.pattern.slice(start, current.index)])
  328. return style
  329. }
  330. function parsePluralOffset (current/*: Context */)/*: number */ {
  331. var pattern = current.pattern
  332. var length = pattern.length
  333. var offset = 0
  334. if (pattern.slice(current.index, current.index + OFFSET.length) === OFFSET) {
  335. if (current.tokens) current.tokens.push(['offset', 'offset'], ['syntax', ':'])
  336. current.index += OFFSET.length // move passed offset:
  337. skipWhitespace(current)
  338. var start = current.index
  339. while (current.index < length && isDigit(pattern.charCodeAt(current.index))) {
  340. ++current.index
  341. }
  342. if (start === current.index) throw expected(current, 'offset number')
  343. if (current.tokens) current.tokens.push(['number', pattern.slice(start, current.index)])
  344. offset = +pattern.slice(start, current.index)
  345. }
  346. return offset
  347. }
  348. function isDigit (code/*: number */)/*: boolean */ {
  349. return (code >= 0x30 && code <= 0x39)
  350. }
  351. function parseSubMessages (current/*: Context */, parentType/*: string */)/*: SubMessages */ {
  352. var pattern = current.pattern
  353. var length = pattern.length
  354. var options/*: SubMessages */ = {}
  355. while (current.index < length && pattern[current.index] !== ARG_CLS) {
  356. var selector = parseId(current)
  357. if (!selector) throw expected(current, 'sub-message selector')
  358. if (current.tokens) current.tokens.push(['selector', selector])
  359. skipWhitespace(current)
  360. options[selector] = parseSubMessage(current, parentType)
  361. skipWhitespace(current)
  362. }
  363. if (!options.other && submTypes.indexOf(parentType) >= 0) {
  364. throw expected(current, null, null, '"other" sub-message must be specified in ' + parentType)
  365. }
  366. return options
  367. }
  368. function parseSubMessage (current/*: Context */, parentType/*: string */)/*: AST */ {
  369. if (current.pattern[current.index] !== ARG_OPN) throw expected(current, ARG_OPN + ' to start sub-message')
  370. if (current.tokens) current.tokens.push(['syntax', ARG_OPN])
  371. ++current.index // move passed {
  372. var message = parseAST(current, parentType)
  373. if (current.pattern[current.index] !== ARG_CLS) throw expected(current, ARG_CLS + ' to end sub-message')
  374. if (current.tokens) current.tokens.push(['syntax', ARG_CLS])
  375. ++current.index // move passed }
  376. return message
  377. }
  378. function expected (current/*: Context */, expected/*:: ?: ?string */, found/*:: ?: ?string */, message/*:: ?: string */) {
  379. var pattern = current.pattern
  380. var lines = pattern.slice(0, current.index).split(/\r?\n/)
  381. var offset = current.index
  382. var line = lines.length
  383. var column = lines.slice(-1)[0].length
  384. found = found || (
  385. (current.index >= pattern.length) ? 'end of message pattern'
  386. : (parseId(current) || pattern[current.index])
  387. )
  388. if (!message) message = errorMessage(expected, found)
  389. message += ' in ' + pattern.replace(/\r?\n/g, '\n')
  390. return new SyntaxError(message, expected, found, offset, line, column)
  391. }
  392. function errorMessage (expected/*: ?string */, found/* string */) {
  393. if (!expected) return 'Unexpected ' + found + ' found'
  394. return 'Expected ' + expected + ' but found ' + found
  395. }
  396. /**
  397. * SyntaxError
  398. * Holds information about bad syntax found in a message pattern
  399. **/
  400. function SyntaxError (message/*: string */, expected/*: ?string */, found/*: ?string */, offset/*: number */, line/*: number */, column/*: number */) {
  401. Error.call(this, message)
  402. this.name = 'SyntaxError'
  403. this.message = message
  404. this.expected = expected
  405. this.found = found
  406. this.offset = offset
  407. this.line = line
  408. this.column = column
  409. }
  410. SyntaxError.prototype = Object.create(Error.prototype)
  411. exports.SyntaxError = SyntaxError