1/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
2
3/* Copyright 2019 Mozilla Foundation and others
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18
19/* fluent-syntax@0.12.0 */
20
21/*
22 * Base class for all Fluent AST nodes.
23 *
24 * All productions described in the ASDL subclass BaseNode, including Span and
25 * Annotation.
26 *
27 */
28class BaseNode {
29  constructor() {}
30
31  equals(other, ignoredFields = ["span"]) {
32    const thisKeys = new Set(Object.keys(this));
33    const otherKeys = new Set(Object.keys(other));
34    if (ignoredFields) {
35      for (const fieldName of ignoredFields) {
36        thisKeys.delete(fieldName);
37        otherKeys.delete(fieldName);
38      }
39    }
40    if (thisKeys.size !== otherKeys.size) {
41      return false;
42    }
43    for (const fieldName of thisKeys) {
44      if (!otherKeys.has(fieldName)) {
45        return false;
46      }
47      const thisVal = this[fieldName];
48      const otherVal = other[fieldName];
49      if (typeof thisVal !== typeof otherVal) {
50        return false;
51      }
52      if (thisVal instanceof Array) {
53        if (thisVal.length !== otherVal.length) {
54          return false;
55        }
56        for (let i = 0; i < thisVal.length; ++i) {
57          if (!scalarsEqual(thisVal[i], otherVal[i], ignoredFields)) {
58            return false;
59          }
60        }
61      } else if (!scalarsEqual(thisVal, otherVal, ignoredFields)) {
62        return false;
63      }
64    }
65    return true;
66  }
67
68  clone() {
69    function visit(value) {
70      if (value instanceof BaseNode) {
71        return value.clone();
72      }
73      if (Array.isArray(value)) {
74        return value.map(visit);
75      }
76      return value;
77    }
78    const clone = Object.create(this.constructor.prototype);
79    for (const prop of Object.keys(this)) {
80      clone[prop] = visit(this[prop]);
81    }
82    return clone;
83  }
84}
85
86function scalarsEqual(thisVal, otherVal, ignoredFields) {
87  if (thisVal instanceof BaseNode) {
88    return thisVal.equals(otherVal, ignoredFields);
89  }
90  return thisVal === otherVal;
91}
92
93/*
94 * Base class for AST nodes which can have Spans.
95 */
96class SyntaxNode extends BaseNode {
97  addSpan(start, end) {
98    this.span = new Span(start, end);
99  }
100}
101
102class Resource extends SyntaxNode {
103  constructor(body = []) {
104    super();
105    this.type = "Resource";
106    this.body = body;
107  }
108}
109
110/*
111 * An abstract base class for useful elements of Resource.body.
112 */
113class Entry extends SyntaxNode {}
114
115class Message extends Entry {
116  constructor(id, value = null, attributes = [], comment = null) {
117    super();
118    this.type = "Message";
119    this.id = id;
120    this.value = value;
121    this.attributes = attributes;
122    this.comment = comment;
123  }
124}
125
126class Term extends Entry {
127  constructor(id, value, attributes = [], comment = null) {
128    super();
129    this.type = "Term";
130    this.id = id;
131    this.value = value;
132    this.attributes = attributes;
133    this.comment = comment;
134  }
135}
136
137class Pattern extends SyntaxNode {
138  constructor(elements) {
139    super();
140    this.type = "Pattern";
141    this.elements = elements;
142  }
143}
144
145/*
146 * An abstract base class for elements of Patterns.
147 */
148class PatternElement extends SyntaxNode {}
149
150class TextElement extends PatternElement {
151  constructor(value) {
152    super();
153    this.type = "TextElement";
154    this.value = value;
155  }
156}
157
158class Placeable extends PatternElement {
159  constructor(expression) {
160    super();
161    this.type = "Placeable";
162    this.expression = expression;
163  }
164}
165
166/*
167 * An abstract base class for expressions.
168 */
169class Expression extends SyntaxNode {}
170
171// An abstract base class for Literals.
172class Literal extends Expression {
173  constructor(value) {
174    super();
175    // The "value" field contains the exact contents of the literal,
176    // character-for-character.
177    this.value = value;
178  }
179
180  parse() {
181    return {value: this.value};
182  }
183}
184
185class StringLiteral extends Literal {
186  constructor(value) {
187    super(value);
188    this.type = "StringLiteral";
189  }
190
191  parse() {
192    // Backslash backslash, backslash double quote, uHHHH, UHHHHHH.
193    const KNOWN_ESCAPES =
194      /(?:\\\\|\\"|\\u([0-9a-fA-F]{4})|\\U([0-9a-fA-F]{6}))/g;
195
196    function from_escape_sequence(match, codepoint4, codepoint6) {
197      switch (match) {
198        case "\\\\":
199          return "\\";
200        case "\\\"":
201          return "\"";
202        default:
203          let codepoint = parseInt(codepoint4 || codepoint6, 16);
204          if (codepoint <= 0xD7FF || 0xE000 <= codepoint) {
205            // It's a Unicode scalar value.
206            return String.fromCodePoint(codepoint);
207          }
208          // Escape sequences reresenting surrogate code points are
209          // well-formed but invalid in Fluent. Replace them with U+FFFD
210          // REPLACEMENT CHARACTER.
211          return "�";
212      }
213    }
214
215    let value = this.value.replace(KNOWN_ESCAPES, from_escape_sequence);
216    return {value};
217  }
218}
219
220class NumberLiteral extends Literal {
221  constructor(value) {
222    super(value);
223    this.type = "NumberLiteral";
224  }
225
226  parse() {
227    let value = parseFloat(this.value);
228    let decimal_position = this.value.indexOf(".");
229    let precision = decimal_position > 0
230      ? this.value.length - decimal_position - 1
231      : 0;
232    return {value, precision};
233  }
234}
235
236class MessageReference extends Expression {
237  constructor(id, attribute = null) {
238    super();
239    this.type = "MessageReference";
240    this.id = id;
241    this.attribute = attribute;
242  }
243}
244
245class TermReference extends Expression {
246  constructor(id, attribute = null, args = null) {
247    super();
248    this.type = "TermReference";
249    this.id = id;
250    this.attribute = attribute;
251    this.arguments = args;
252  }
253}
254
255class VariableReference extends Expression {
256  constructor(id) {
257    super();
258    this.type = "VariableReference";
259    this.id = id;
260  }
261}
262
263class FunctionReference extends Expression {
264  constructor(id, args) {
265    super();
266    this.type = "FunctionReference";
267    this.id = id;
268    this.arguments = args;
269  }
270}
271
272class SelectExpression extends Expression {
273  constructor(selector, variants) {
274    super();
275    this.type = "SelectExpression";
276    this.selector = selector;
277    this.variants = variants;
278  }
279}
280
281class CallArguments extends SyntaxNode {
282  constructor(positional = [], named = []) {
283    super();
284    this.type = "CallArguments";
285    this.positional = positional;
286    this.named = named;
287  }
288}
289
290class Attribute extends SyntaxNode {
291  constructor(id, value) {
292    super();
293    this.type = "Attribute";
294    this.id = id;
295    this.value = value;
296  }
297}
298
299class Variant extends SyntaxNode {
300  constructor(key, value, def = false) {
301    super();
302    this.type = "Variant";
303    this.key = key;
304    this.value = value;
305    this.default = def;
306  }
307}
308
309class NamedArgument extends SyntaxNode {
310  constructor(name, value) {
311    super();
312    this.type = "NamedArgument";
313    this.name = name;
314    this.value = value;
315  }
316}
317
318class Identifier extends SyntaxNode {
319  constructor(name) {
320    super();
321    this.type = "Identifier";
322    this.name = name;
323  }
324}
325
326class BaseComment extends Entry {
327  constructor(content) {
328    super();
329    this.type = "BaseComment";
330    this.content = content;
331  }
332}
333
334class Comment extends BaseComment {
335  constructor(content) {
336    super(content);
337    this.type = "Comment";
338  }
339}
340
341class GroupComment extends BaseComment {
342  constructor(content) {
343    super(content);
344    this.type = "GroupComment";
345  }
346}
347class ResourceComment extends BaseComment {
348  constructor(content) {
349    super(content);
350    this.type = "ResourceComment";
351  }
352}
353
354class Junk extends SyntaxNode {
355  constructor(content) {
356    super();
357    this.type = "Junk";
358    this.annotations = [];
359    this.content = content;
360  }
361
362  addAnnotation(annot) {
363    this.annotations.push(annot);
364  }
365}
366
367class Span extends BaseNode {
368  constructor(start, end) {
369    super();
370    this.type = "Span";
371    this.start = start;
372    this.end = end;
373  }
374}
375
376class Annotation extends SyntaxNode {
377  constructor(code, args = [], message) {
378    super();
379    this.type = "Annotation";
380    this.code = code;
381    this.arguments = args;
382    this.message = message;
383  }
384}
385
386const ast = ({
387  BaseNode: BaseNode,
388  Resource: Resource,
389  Entry: Entry,
390  Message: Message,
391  Term: Term,
392  Pattern: Pattern,
393  PatternElement: PatternElement,
394  TextElement: TextElement,
395  Placeable: Placeable,
396  Expression: Expression,
397  Literal: Literal,
398  StringLiteral: StringLiteral,
399  NumberLiteral: NumberLiteral,
400  MessageReference: MessageReference,
401  TermReference: TermReference,
402  VariableReference: VariableReference,
403  FunctionReference: FunctionReference,
404  SelectExpression: SelectExpression,
405  CallArguments: CallArguments,
406  Attribute: Attribute,
407  Variant: Variant,
408  NamedArgument: NamedArgument,
409  Identifier: Identifier,
410  BaseComment: BaseComment,
411  Comment: Comment,
412  GroupComment: GroupComment,
413  ResourceComment: ResourceComment,
414  Junk: Junk,
415  Span: Span,
416  Annotation: Annotation
417});
418
419class ParseError extends Error {
420  constructor(code, ...args) {
421    super();
422    this.code = code;
423    this.args = args;
424    this.message = getErrorMessage(code, args);
425  }
426}
427
428/* eslint-disable complexity */
429function getErrorMessage(code, args) {
430  switch (code) {
431    case "E0001":
432      return "Generic error";
433    case "E0002":
434      return "Expected an entry start";
435    case "E0003": {
436      const [token] = args;
437      return `Expected token: "${token}"`;
438    }
439    case "E0004": {
440      const [range] = args;
441      return `Expected a character from range: "${range}"`;
442    }
443    case "E0005": {
444      const [id] = args;
445      return `Expected message "${id}" to have a value or attributes`;
446    }
447    case "E0006": {
448      const [id] = args;
449      return `Expected term "-${id}" to have a value`;
450    }
451    case "E0007":
452      return "Keyword cannot end with a whitespace";
453    case "E0008":
454      return "The callee has to be an upper-case identifier or a term";
455    case "E0009":
456      return "The argument name has to be a simple identifier";
457    case "E0010":
458      return "Expected one of the variants to be marked as default (*)";
459    case "E0011":
460      return 'Expected at least one variant after "->"';
461    case "E0012":
462      return "Expected value";
463    case "E0013":
464      return "Expected variant key";
465    case "E0014":
466      return "Expected literal";
467    case "E0015":
468      return "Only one variant can be marked as default (*)";
469    case "E0016":
470      return "Message references cannot be used as selectors";
471    case "E0017":
472      return "Terms cannot be used as selectors";
473    case "E0018":
474      return "Attributes of messages cannot be used as selectors";
475    case "E0019":
476      return "Attributes of terms cannot be used as placeables";
477    case "E0020":
478      return "Unterminated string expression";
479    case "E0021":
480      return "Positional arguments must not follow named arguments";
481    case "E0022":
482      return "Named arguments must be unique";
483    case "E0024":
484      return "Cannot access variants of a message.";
485    case "E0025": {
486      const [char] = args;
487      return `Unknown escape sequence: \\${char}.`;
488    }
489    case "E0026": {
490      const [sequence] = args;
491      return `Invalid Unicode escape sequence: ${sequence}.`;
492    }
493    case "E0027":
494      return "Unbalanced closing brace in TextElement.";
495    case "E0028":
496      return "Expected an inline expression";
497    default:
498      return code;
499  }
500}
501
502function includes(arr, elem) {
503  return arr.indexOf(elem) > -1;
504}
505
506/* eslint no-magic-numbers: "off" */
507
508class ParserStream {
509  constructor(string) {
510    this.string = string;
511    this.index = 0;
512    this.peekOffset = 0;
513  }
514
515  charAt(offset) {
516    // When the cursor is at CRLF, return LF but don't move the cursor.
517    // The cursor still points to the EOL position, which in this case is the
518    // beginning of the compound CRLF sequence. This ensures slices of
519    // [inclusive, exclusive) continue to work properly.
520    if (this.string[offset] === "\r"
521        && this.string[offset + 1] === "\n") {
522      return "\n";
523    }
524
525    return this.string[offset];
526  }
527
528  get currentChar() {
529    return this.charAt(this.index);
530  }
531
532  get currentPeek() {
533    return this.charAt(this.index + this.peekOffset);
534  }
535
536  next() {
537    this.peekOffset = 0;
538    // Skip over the CRLF as if it was a single character.
539    if (this.string[this.index] === "\r"
540        && this.string[this.index + 1] === "\n") {
541      this.index++;
542    }
543    this.index++;
544    return this.string[this.index];
545  }
546
547  peek() {
548    // Skip over the CRLF as if it was a single character.
549    if (this.string[this.index + this.peekOffset] === "\r"
550        && this.string[this.index + this.peekOffset + 1] === "\n") {
551      this.peekOffset++;
552    }
553    this.peekOffset++;
554    return this.string[this.index + this.peekOffset];
555  }
556
557  resetPeek(offset = 0) {
558    this.peekOffset = offset;
559  }
560
561  skipToPeek() {
562    this.index += this.peekOffset;
563    this.peekOffset = 0;
564  }
565}
566
567const EOL = "\n";
568const EOF = undefined;
569const SPECIAL_LINE_START_CHARS = ["}", ".", "[", "*"];
570
571class FluentParserStream extends ParserStream {
572  peekBlankInline() {
573    const start = this.index + this.peekOffset;
574    while (this.currentPeek === " ") {
575      this.peek();
576    }
577    return this.string.slice(start, this.index + this.peekOffset);
578  }
579
580  skipBlankInline() {
581    const blank = this.peekBlankInline();
582    this.skipToPeek();
583    return blank;
584  }
585
586  peekBlankBlock() {
587    let blank = "";
588    while (true) {
589      const lineStart = this.peekOffset;
590      this.peekBlankInline();
591      if (this.currentPeek === EOL) {
592        blank += EOL;
593        this.peek();
594        continue;
595      }
596      if (this.currentPeek === EOF) {
597        // Treat the blank line at EOF as a blank block.
598        return blank;
599      }
600      // Any other char; reset to column 1 on this line.
601      this.resetPeek(lineStart);
602      return blank;
603    }
604  }
605
606  skipBlankBlock() {
607    const blank = this.peekBlankBlock();
608    this.skipToPeek();
609    return blank;
610  }
611
612  peekBlank() {
613    while (this.currentPeek === " " || this.currentPeek === EOL) {
614      this.peek();
615    }
616  }
617
618  skipBlank() {
619    this.peekBlank();
620    this.skipToPeek();
621  }
622
623  expectChar(ch) {
624    if (this.currentChar === ch) {
625      this.next();
626      return true;
627    }
628
629    throw new ParseError("E0003", ch);
630  }
631
632  expectLineEnd() {
633    if (this.currentChar === EOF) {
634      // EOF is a valid line end in Fluent.
635      return true;
636    }
637
638    if (this.currentChar === EOL) {
639      this.next();
640      return true;
641    }
642
643    // Unicode Character 'SYMBOL FOR NEWLINE' (U+2424)
644    throw new ParseError("E0003", "\u2424");
645  }
646
647  takeChar(f) {
648    const ch = this.currentChar;
649    if (ch === EOF) {
650      return EOF;
651    }
652    if (f(ch)) {
653      this.next();
654      return ch;
655    }
656    return null;
657  }
658
659  isCharIdStart(ch) {
660    if (ch === EOF) {
661      return false;
662    }
663
664    const cc = ch.charCodeAt(0);
665    return (cc >= 97 && cc <= 122) || // a-z
666           (cc >= 65 && cc <= 90); // A-Z
667  }
668
669  isIdentifierStart() {
670    return this.isCharIdStart(this.currentPeek);
671  }
672
673  isNumberStart() {
674    const ch = this.currentChar === "-"
675      ? this.peek()
676      : this.currentChar;
677
678    if (ch === EOF) {
679      this.resetPeek();
680      return false;
681    }
682
683    const cc = ch.charCodeAt(0);
684    const isDigit = cc >= 48 && cc <= 57; // 0-9
685    this.resetPeek();
686    return isDigit;
687  }
688
689  isCharPatternContinuation(ch) {
690    if (ch === EOF) {
691      return false;
692    }
693
694    return !includes(SPECIAL_LINE_START_CHARS, ch);
695  }
696
697  isValueStart() {
698    // Inline Patterns may start with any char.
699    const ch = this.currentPeek;
700    return ch !== EOL && ch !== EOF;
701  }
702
703  isValueContinuation() {
704    const column1 = this.peekOffset;
705    this.peekBlankInline();
706
707    if (this.currentPeek === "{") {
708      this.resetPeek(column1);
709      return true;
710    }
711
712    if (this.peekOffset - column1 === 0) {
713      return false;
714    }
715
716    if (this.isCharPatternContinuation(this.currentPeek)) {
717      this.resetPeek(column1);
718      return true;
719    }
720
721    return false;
722  }
723
724  // -1 - any
725  //  0 - comment
726  //  1 - group comment
727  //  2 - resource comment
728  isNextLineComment(level = -1) {
729    if (this.currentChar !== EOL) {
730      return false;
731    }
732
733    let i = 0;
734
735    while (i <= level || (level === -1 && i < 3)) {
736      if (this.peek() !== "#") {
737        if (i <= level && level !== -1) {
738          this.resetPeek();
739          return false;
740        }
741        break;
742      }
743      i++;
744    }
745
746    // The first char after #, ## or ###.
747    const ch = this.peek();
748    if (ch === " " || ch === EOL) {
749      this.resetPeek();
750      return true;
751    }
752
753    this.resetPeek();
754    return false;
755  }
756
757  isVariantStart() {
758    const currentPeekOffset = this.peekOffset;
759    if (this.currentPeek === "*") {
760      this.peek();
761    }
762    if (this.currentPeek === "[") {
763      this.resetPeek(currentPeekOffset);
764      return true;
765    }
766    this.resetPeek(currentPeekOffset);
767    return false;
768  }
769
770  isAttributeStart() {
771    return this.currentPeek === ".";
772  }
773
774  skipToNextEntryStart(junkStart) {
775    let lastNewline = this.string.lastIndexOf(EOL, this.index);
776    if (junkStart < lastNewline) {
777      // Last seen newline is _after_ the junk start. It's safe to rewind
778      // without the risk of resuming at the same broken entry.
779      this.index = lastNewline;
780    }
781    while (this.currentChar) {
782      // We're only interested in beginnings of line.
783      if (this.currentChar !== EOL) {
784        this.next();
785        continue;
786      }
787
788      // Break if the first char in this line looks like an entry start.
789      const first = this.next();
790      if (this.isCharIdStart(first) || first === "-" || first === "#") {
791        break;
792      }
793    }
794  }
795
796  takeIDStart() {
797    if (this.isCharIdStart(this.currentChar)) {
798      const ret = this.currentChar;
799      this.next();
800      return ret;
801    }
802
803    throw new ParseError("E0004", "a-zA-Z");
804  }
805
806  takeIDChar() {
807    const closure = ch => {
808      const cc = ch.charCodeAt(0);
809      return ((cc >= 97 && cc <= 122) || // a-z
810              (cc >= 65 && cc <= 90) || // A-Z
811              (cc >= 48 && cc <= 57) || // 0-9
812               cc === 95 || cc === 45); // _-
813    };
814
815    return this.takeChar(closure);
816  }
817
818  takeDigit() {
819    const closure = ch => {
820      const cc = ch.charCodeAt(0);
821      return (cc >= 48 && cc <= 57); // 0-9
822    };
823
824    return this.takeChar(closure);
825  }
826
827  takeHexDigit() {
828    const closure = ch => {
829      const cc = ch.charCodeAt(0);
830      return (cc >= 48 && cc <= 57) // 0-9
831        || (cc >= 65 && cc <= 70) // A-F
832        || (cc >= 97 && cc <= 102); // a-f
833    };
834
835    return this.takeChar(closure);
836  }
837}
838
839/*  eslint no-magic-numbers: [0]  */
840
841
842const trailingWSRe = /[ \t\n\r]+$/;
843
844
845function withSpan(fn) {
846  return function(ps, ...args) {
847    if (!this.withSpans) {
848      return fn.call(this, ps, ...args);
849    }
850
851    const start = ps.index;
852    const node = fn.call(this, ps, ...args);
853
854    // Don't re-add the span if the node already has it. This may happen when
855    // one decorated function calls another decorated function.
856    if (node.span) {
857      return node;
858    }
859
860    const end = ps.index;
861    node.addSpan(start, end);
862    return node;
863  };
864}
865
866
867class FluentParser {
868  constructor({
869    withSpans = true,
870  } = {}) {
871    this.withSpans = withSpans;
872
873    // Poor man's decorators.
874    const methodNames = [
875      "getComment", "getMessage", "getTerm", "getAttribute", "getIdentifier",
876      "getVariant", "getNumber", "getPattern", "getTextElement",
877      "getPlaceable", "getExpression", "getInlineExpression",
878      "getCallArgument", "getCallArguments", "getString", "getLiteral",
879    ];
880    for (const name of methodNames) {
881      this[name] = withSpan(this[name]);
882    }
883  }
884
885  parse(source) {
886    const ps = new FluentParserStream(source);
887    ps.skipBlankBlock();
888
889    const entries = [];
890    let lastComment = null;
891
892    while (ps.currentChar) {
893      const entry = this.getEntryOrJunk(ps);
894      const blankLines = ps.skipBlankBlock();
895
896      // Regular Comments require special logic. Comments may be attached to
897      // Messages or Terms if they are followed immediately by them. However
898      // they should parse as standalone when they're followed by Junk.
899      // Consequently, we only attach Comments once we know that the Message
900      // or the Term parsed successfully.
901      if (entry.type === "Comment"
902          && blankLines.length === 0
903          && ps.currentChar) {
904        // Stash the comment and decide what to do with it in the next pass.
905        lastComment = entry;
906        continue;
907      }
908
909      if (lastComment) {
910        if (entry.type === "Message" || entry.type === "Term") {
911          entry.comment = lastComment;
912          if (this.withSpans) {
913            entry.span.start = entry.comment.span.start;
914          }
915        } else {
916          entries.push(lastComment);
917        }
918        // In either case, the stashed comment has been dealt with; clear it.
919        lastComment = null;
920      }
921
922      // No special logic for other types of entries.
923      entries.push(entry);
924    }
925
926    const res = new Resource(entries);
927
928    if (this.withSpans) {
929      res.addSpan(0, ps.index);
930    }
931
932    return res;
933  }
934
935  /*
936   * Parse the first Message or Term in `source`.
937   *
938   * Skip all encountered comments and start parsing at the first Message or
939   * Term start. Return Junk if the parsing is not successful.
940   *
941   * Preceding comments are ignored unless they contain syntax errors
942   * themselves, in which case Junk for the invalid comment is returned.
943   */
944  parseEntry(source) {
945    const ps = new FluentParserStream(source);
946    ps.skipBlankBlock();
947
948    while (ps.currentChar === "#") {
949      const skipped = this.getEntryOrJunk(ps);
950      if (skipped.type === "Junk") {
951        // Don't skip Junk comments.
952        return skipped;
953      }
954      ps.skipBlankBlock();
955    }
956
957    return this.getEntryOrJunk(ps);
958  }
959
960  getEntryOrJunk(ps) {
961    const entryStartPos = ps.index;
962
963    try {
964      const entry = this.getEntry(ps);
965      ps.expectLineEnd();
966      return entry;
967    } catch (err) {
968      if (!(err instanceof ParseError)) {
969        throw err;
970      }
971
972      let errorIndex = ps.index;
973      ps.skipToNextEntryStart(entryStartPos);
974      const nextEntryStart = ps.index;
975      if (nextEntryStart < errorIndex) {
976        // The position of the error must be inside of the Junk's span.
977        errorIndex = nextEntryStart;
978      }
979
980      // Create a Junk instance
981      const slice = ps.string.substring(entryStartPos, nextEntryStart);
982      const junk = new Junk(slice);
983      if (this.withSpans) {
984        junk.addSpan(entryStartPos, nextEntryStart);
985      }
986      const annot = new Annotation(err.code, err.args, err.message);
987      annot.addSpan(errorIndex, errorIndex);
988      junk.addAnnotation(annot);
989      return junk;
990    }
991  }
992
993  getEntry(ps) {
994    if (ps.currentChar === "#") {
995      return this.getComment(ps);
996    }
997
998    if (ps.currentChar === "-") {
999      return this.getTerm(ps);
1000    }
1001
1002    if (ps.isIdentifierStart()) {
1003      return this.getMessage(ps);
1004    }
1005
1006    throw new ParseError("E0002");
1007  }
1008
1009  getComment(ps) {
1010    // 0 - comment
1011    // 1 - group comment
1012    // 2 - resource comment
1013    let level = -1;
1014    let content = "";
1015
1016    while (true) {
1017      let i = -1;
1018      while (ps.currentChar === "#" && (i < (level === -1 ? 2 : level))) {
1019        ps.next();
1020        i++;
1021      }
1022
1023      if (level === -1) {
1024        level = i;
1025      }
1026
1027      if (ps.currentChar !== EOL) {
1028        ps.expectChar(" ");
1029        let ch;
1030        while ((ch = ps.takeChar(x => x !== EOL))) {
1031          content += ch;
1032        }
1033      }
1034
1035      if (ps.isNextLineComment(level)) {
1036        content += ps.currentChar;
1037        ps.next();
1038      } else {
1039        break;
1040      }
1041    }
1042
1043    let Comment$$1;
1044    switch (level) {
1045      case 0:
1046        Comment$$1 = Comment;
1047        break;
1048      case 1:
1049        Comment$$1 = GroupComment;
1050        break;
1051      case 2:
1052        Comment$$1 = ResourceComment;
1053        break;
1054    }
1055    return new Comment$$1(content);
1056  }
1057
1058  getMessage(ps) {
1059    const id = this.getIdentifier(ps);
1060
1061    ps.skipBlankInline();
1062    ps.expectChar("=");
1063
1064    const value = this.maybeGetPattern(ps);
1065    const attrs = this.getAttributes(ps);
1066
1067    if (value === null && attrs.length === 0) {
1068      throw new ParseError("E0005", id.name);
1069    }
1070
1071    return new Message(id, value, attrs);
1072  }
1073
1074  getTerm(ps) {
1075    ps.expectChar("-");
1076    const id = this.getIdentifier(ps);
1077
1078    ps.skipBlankInline();
1079    ps.expectChar("=");
1080
1081    const value = this.maybeGetPattern(ps);
1082    if (value === null) {
1083      throw new ParseError("E0006", id.name);
1084    }
1085
1086    const attrs = this.getAttributes(ps);
1087    return new Term(id, value, attrs);
1088  }
1089
1090  getAttribute(ps) {
1091    ps.expectChar(".");
1092
1093    const key = this.getIdentifier(ps);
1094
1095    ps.skipBlankInline();
1096    ps.expectChar("=");
1097
1098    const value = this.maybeGetPattern(ps);
1099    if (value === null) {
1100      throw new ParseError("E0012");
1101    }
1102
1103    return new Attribute(key, value);
1104  }
1105
1106  getAttributes(ps) {
1107    const attrs = [];
1108    ps.peekBlank();
1109    while (ps.isAttributeStart()) {
1110      ps.skipToPeek();
1111      const attr = this.getAttribute(ps);
1112      attrs.push(attr);
1113      ps.peekBlank();
1114    }
1115    return attrs;
1116  }
1117
1118  getIdentifier(ps) {
1119    let name = ps.takeIDStart();
1120
1121    let ch;
1122    while ((ch = ps.takeIDChar())) {
1123      name += ch;
1124    }
1125
1126    return new Identifier(name);
1127  }
1128
1129  getVariantKey(ps) {
1130    const ch = ps.currentChar;
1131
1132    if (ch === EOF) {
1133      throw new ParseError("E0013");
1134    }
1135
1136    const cc = ch.charCodeAt(0);
1137
1138    if ((cc >= 48 && cc <= 57) || cc === 45) { // 0-9, -
1139      return this.getNumber(ps);
1140    }
1141
1142    return this.getIdentifier(ps);
1143  }
1144
1145  getVariant(ps, {hasDefault}) {
1146    let defaultIndex = false;
1147
1148    if (ps.currentChar === "*") {
1149      if (hasDefault) {
1150        throw new ParseError("E0015");
1151      }
1152      ps.next();
1153      defaultIndex = true;
1154    }
1155
1156    ps.expectChar("[");
1157
1158    ps.skipBlank();
1159
1160    const key = this.getVariantKey(ps);
1161
1162    ps.skipBlank();
1163    ps.expectChar("]");
1164
1165    const value = this.maybeGetPattern(ps);
1166    if (value === null) {
1167      throw new ParseError("E0012");
1168    }
1169
1170    return new Variant(key, value, defaultIndex);
1171  }
1172
1173  getVariants(ps) {
1174    const variants = [];
1175    let hasDefault = false;
1176
1177    ps.skipBlank();
1178    while (ps.isVariantStart()) {
1179      const variant = this.getVariant(ps, {hasDefault});
1180
1181      if (variant.default) {
1182        hasDefault = true;
1183      }
1184
1185      variants.push(variant);
1186      ps.expectLineEnd();
1187      ps.skipBlank();
1188    }
1189
1190    if (variants.length === 0) {
1191      throw new ParseError("E0011");
1192    }
1193
1194    if (!hasDefault) {
1195      throw new ParseError("E0010");
1196    }
1197
1198    return variants;
1199  }
1200
1201  getDigits(ps) {
1202    let num = "";
1203
1204    let ch;
1205    while ((ch = ps.takeDigit())) {
1206      num += ch;
1207    }
1208
1209    if (num.length === 0) {
1210      throw new ParseError("E0004", "0-9");
1211    }
1212
1213    return num;
1214  }
1215
1216  getNumber(ps) {
1217    let value = "";
1218
1219    if (ps.currentChar === "-") {
1220      ps.next();
1221      value += `-${this.getDigits(ps)}`;
1222    } else {
1223      value += this.getDigits(ps);
1224    }
1225
1226    if (ps.currentChar === ".") {
1227      ps.next();
1228      value += `.${this.getDigits(ps)}`;
1229    }
1230
1231    return new NumberLiteral(value);
1232  }
1233
1234  // maybeGetPattern distinguishes between patterns which start on the same line
1235  // as the identifier (a.k.a. inline signleline patterns and inline multiline
1236  // patterns) and patterns which start on a new line (a.k.a. block multiline
1237  // patterns). The distinction is important for the dedentation logic: the
1238  // indent of the first line of a block pattern must be taken into account when
1239  // calculating the maximum common indent.
1240  maybeGetPattern(ps) {
1241    ps.peekBlankInline();
1242    if (ps.isValueStart()) {
1243      ps.skipToPeek();
1244      return this.getPattern(ps, {isBlock: false});
1245    }
1246
1247    ps.peekBlankBlock();
1248    if (ps.isValueContinuation()) {
1249      ps.skipToPeek();
1250      return this.getPattern(ps, {isBlock: true});
1251    }
1252
1253    return null;
1254  }
1255
1256  getPattern(ps, {isBlock}) {
1257    const elements = [];
1258    if (isBlock) {
1259      // A block pattern is a pattern which starts on a new line. Store and
1260      // measure the indent of this first line for the dedentation logic.
1261      const blankStart = ps.index;
1262      const firstIndent = ps.skipBlankInline();
1263      elements.push(this.getIndent(ps, firstIndent, blankStart));
1264      var commonIndentLength = firstIndent.length;
1265    } else {
1266      commonIndentLength = Infinity;
1267    }
1268
1269    let ch;
1270    elements: while ((ch = ps.currentChar)) {
1271      switch (ch) {
1272        case EOL: {
1273          const blankStart = ps.index;
1274          const blankLines = ps.peekBlankBlock();
1275          if (ps.isValueContinuation()) {
1276            ps.skipToPeek();
1277            const indent = ps.skipBlankInline();
1278            commonIndentLength = Math.min(commonIndentLength, indent.length);
1279            elements.push(this.getIndent(ps, blankLines + indent, blankStart));
1280            continue elements;
1281          }
1282
1283          // The end condition for getPattern's while loop is a newline
1284          // which is not followed by a valid pattern continuation.
1285          ps.resetPeek();
1286          break elements;
1287        }
1288        case "{":
1289          elements.push(this.getPlaceable(ps));
1290          continue elements;
1291        case "}":
1292          throw new ParseError("E0027");
1293        default:
1294          const element = this.getTextElement(ps);
1295          elements.push(element);
1296      }
1297    }
1298
1299    const dedented = this.dedent(elements, commonIndentLength);
1300    return new Pattern(dedented);
1301  }
1302
1303  // Create a token representing an indent. It's not part of the AST and it will
1304  // be trimmed and merged into adjacent TextElements, or turned into a new
1305  // TextElement, if it's surrounded by two Placeables.
1306  getIndent(ps, value, start) {
1307    return {
1308      type: "Indent",
1309      span: {start, end: ps.index},
1310      value,
1311    };
1312  }
1313
1314  // Dedent a list of elements by removing the maximum common indent from the
1315  // beginning of text lines. The common indent is calculated in getPattern.
1316  dedent(elements, commonIndent) {
1317    const trimmed = [];
1318
1319    for (let element of elements) {
1320      if (element.type === "Placeable") {
1321        trimmed.push(element);
1322        continue;
1323      }
1324
1325      if (element.type === "Indent") {
1326        // Strip common indent.
1327        element.value = element.value.slice(
1328          0, element.value.length - commonIndent);
1329        if (element.value.length === 0) {
1330          continue;
1331        }
1332      }
1333
1334      let prev = trimmed[trimmed.length - 1];
1335      if (prev && prev.type === "TextElement") {
1336        // Join adjacent TextElements by replacing them with their sum.
1337        const sum = new TextElement(prev.value + element.value);
1338        if (this.withSpans) {
1339          sum.addSpan(prev.span.start, element.span.end);
1340        }
1341        trimmed[trimmed.length - 1] = sum;
1342        continue;
1343      }
1344
1345      if (element.type === "Indent") {
1346        // If the indent hasn't been merged into a preceding TextElement,
1347        // convert it into a new TextElement.
1348        const textElement = new TextElement(element.value);
1349        if (this.withSpans) {
1350          textElement.addSpan(element.span.start, element.span.end);
1351        }
1352        element = textElement;
1353      }
1354
1355      trimmed.push(element);
1356    }
1357
1358    // Trim trailing whitespace from the Pattern.
1359    const lastElement = trimmed[trimmed.length - 1];
1360    if (lastElement.type === "TextElement") {
1361      lastElement.value = lastElement.value.replace(trailingWSRe, "");
1362      if (lastElement.value.length === 0) {
1363        trimmed.pop();
1364      }
1365    }
1366
1367    return trimmed;
1368  }
1369
1370  getTextElement(ps) {
1371    let buffer = "";
1372
1373    let ch;
1374    while ((ch = ps.currentChar)) {
1375      if (ch === "{" || ch === "}") {
1376        return new TextElement(buffer);
1377      }
1378
1379      if (ch === EOL) {
1380        return new TextElement(buffer);
1381      }
1382
1383      buffer += ch;
1384      ps.next();
1385    }
1386
1387    return new TextElement(buffer);
1388  }
1389
1390  getEscapeSequence(ps) {
1391    const next = ps.currentChar;
1392
1393    switch (next) {
1394      case "\\":
1395      case "\"":
1396        ps.next();
1397        return `\\${next}`;
1398      case "u":
1399        return this.getUnicodeEscapeSequence(ps, next, 4);
1400      case "U":
1401        return this.getUnicodeEscapeSequence(ps, next, 6);
1402      default:
1403        throw new ParseError("E0025", next);
1404    }
1405  }
1406
1407  getUnicodeEscapeSequence(ps, u, digits) {
1408    ps.expectChar(u);
1409
1410    let sequence = "";
1411    for (let i = 0; i < digits; i++) {
1412      const ch = ps.takeHexDigit();
1413
1414      if (!ch) {
1415        throw new ParseError(
1416          "E0026", `\\${u}${sequence}${ps.currentChar}`);
1417      }
1418
1419      sequence += ch;
1420    }
1421
1422    return `\\${u}${sequence}`;
1423  }
1424
1425  getPlaceable(ps) {
1426    ps.expectChar("{");
1427    ps.skipBlank();
1428    const expression = this.getExpression(ps);
1429    ps.expectChar("}");
1430    return new Placeable(expression);
1431  }
1432
1433  getExpression(ps) {
1434    const selector = this.getInlineExpression(ps);
1435    ps.skipBlank();
1436
1437    if (ps.currentChar === "-") {
1438      if (ps.peek() !== ">") {
1439        ps.resetPeek();
1440        return selector;
1441      }
1442
1443      if (selector.type === "MessageReference") {
1444        if (selector.attribute === null) {
1445          throw new ParseError("E0016");
1446        } else {
1447          throw new ParseError("E0018");
1448        }
1449      }
1450
1451      if (selector.type === "TermReference" && selector.attribute === null) {
1452        throw new ParseError("E0017");
1453      }
1454
1455      ps.next();
1456      ps.next();
1457
1458      ps.skipBlankInline();
1459      ps.expectLineEnd();
1460
1461      const variants = this.getVariants(ps);
1462      return new SelectExpression(selector, variants);
1463    }
1464
1465    if (selector.type === "TermReference" && selector.attribute !== null) {
1466      throw new ParseError("E0019");
1467    }
1468
1469    return selector;
1470  }
1471
1472  getInlineExpression(ps) {
1473    if (ps.currentChar === "{") {
1474      return this.getPlaceable(ps);
1475    }
1476
1477    if (ps.isNumberStart()) {
1478      return this.getNumber(ps);
1479    }
1480
1481    if (ps.currentChar === '"') {
1482      return this.getString(ps);
1483    }
1484
1485    if (ps.currentChar === "$") {
1486      ps.next();
1487      const id = this.getIdentifier(ps);
1488      return new VariableReference(id);
1489    }
1490
1491    if (ps.currentChar === "-") {
1492      ps.next();
1493      const id = this.getIdentifier(ps);
1494
1495      let attr;
1496      if (ps.currentChar === ".") {
1497        ps.next();
1498        attr = this.getIdentifier(ps);
1499      }
1500
1501      let args;
1502      if (ps.currentChar === "(") {
1503        args = this.getCallArguments(ps);
1504      }
1505
1506      return new TermReference(id, attr, args);
1507    }
1508
1509    if (ps.isIdentifierStart()) {
1510      const id = this.getIdentifier(ps);
1511
1512      if (ps.currentChar === "(") {
1513        // It's a Function. Ensure it's all upper-case.
1514        if (!/^[A-Z][A-Z0-9_-]*$/.test(id.name)) {
1515          throw new ParseError("E0008");
1516        }
1517
1518        let args = this.getCallArguments(ps);
1519        return new FunctionReference(id, args);
1520      }
1521
1522      let attr;
1523      if (ps.currentChar === ".") {
1524        ps.next();
1525        attr = this.getIdentifier(ps);
1526      }
1527
1528      return new MessageReference(id, attr);
1529    }
1530
1531
1532    throw new ParseError("E0028");
1533  }
1534
1535  getCallArgument(ps) {
1536    const exp = this.getInlineExpression(ps);
1537
1538    ps.skipBlank();
1539
1540    if (ps.currentChar !== ":") {
1541      return exp;
1542    }
1543
1544    if (exp.type === "MessageReference" && exp.attribute === null) {
1545      ps.next();
1546      ps.skipBlank();
1547
1548      const value = this.getLiteral(ps);
1549      return new NamedArgument(exp.id, value);
1550    }
1551
1552    throw new ParseError("E0009");
1553  }
1554
1555  getCallArguments(ps) {
1556    const positional = [];
1557    const named = [];
1558    const argumentNames = new Set();
1559
1560    ps.expectChar("(");
1561    ps.skipBlank();
1562
1563    while (true) {
1564      if (ps.currentChar === ")") {
1565        break;
1566      }
1567
1568      const arg = this.getCallArgument(ps);
1569      if (arg.type === "NamedArgument") {
1570        if (argumentNames.has(arg.name.name)) {
1571          throw new ParseError("E0022");
1572        }
1573        named.push(arg);
1574        argumentNames.add(arg.name.name);
1575      } else if (argumentNames.size > 0) {
1576        throw new ParseError("E0021");
1577      } else {
1578        positional.push(arg);
1579      }
1580
1581      ps.skipBlank();
1582
1583      if (ps.currentChar === ",") {
1584        ps.next();
1585        ps.skipBlank();
1586        continue;
1587      }
1588
1589      break;
1590    }
1591
1592    ps.expectChar(")");
1593    return new CallArguments(positional, named);
1594  }
1595
1596  getString(ps) {
1597    ps.expectChar("\"");
1598    let value = "";
1599
1600    let ch;
1601    while ((ch = ps.takeChar(x => x !== '"' && x !== EOL))) {
1602      if (ch === "\\") {
1603        value += this.getEscapeSequence(ps);
1604      } else {
1605        value += ch;
1606      }
1607    }
1608
1609    if (ps.currentChar === EOL) {
1610      throw new ParseError("E0020");
1611    }
1612
1613    ps.expectChar("\"");
1614
1615    return new StringLiteral(value);
1616  }
1617
1618  getLiteral(ps) {
1619    if (ps.isNumberStart()) {
1620      return this.getNumber(ps);
1621    }
1622
1623    if (ps.currentChar === '"') {
1624      return this.getString(ps);
1625    }
1626
1627    throw new ParseError("E0014");
1628  }
1629}
1630
1631function indent(content) {
1632  return content.split("\n").join("\n    ");
1633}
1634
1635function includesNewLine(elem) {
1636  return elem.type === "TextElement" && includes(elem.value, "\n");
1637}
1638
1639function isSelectExpr(elem) {
1640  return elem.type === "Placeable"
1641    && elem.expression.type === "SelectExpression";
1642}
1643
1644const HAS_ENTRIES = 1;
1645
1646class FluentSerializer {
1647  constructor({ withJunk = false } = {}) {
1648    this.withJunk = withJunk;
1649  }
1650
1651  serialize(resource) {
1652    if (resource.type !== "Resource") {
1653      throw new Error(`Unknown resource type: ${resource.type}`);
1654    }
1655
1656    let state = 0;
1657    const parts = [];
1658
1659    for (const entry of resource.body) {
1660      if (entry.type !== "Junk" || this.withJunk) {
1661        parts.push(this.serializeEntry(entry, state));
1662        if (!(state & HAS_ENTRIES)) {
1663          state |= HAS_ENTRIES;
1664        }
1665      }
1666    }
1667
1668    return parts.join("");
1669  }
1670
1671  serializeEntry(entry, state = 0) {
1672    switch (entry.type) {
1673      case "Message":
1674        return serializeMessage(entry);
1675      case "Term":
1676        return serializeTerm(entry);
1677      case "Comment":
1678        if (state & HAS_ENTRIES) {
1679          return `\n${serializeComment(entry, "#")}\n`;
1680        }
1681        return `${serializeComment(entry, "#")}\n`;
1682      case "GroupComment":
1683        if (state & HAS_ENTRIES) {
1684          return `\n${serializeComment(entry, "##")}\n`;
1685        }
1686        return `${serializeComment(entry, "##")}\n`;
1687      case "ResourceComment":
1688        if (state & HAS_ENTRIES) {
1689          return `\n${serializeComment(entry, "###")}\n`;
1690        }
1691        return `${serializeComment(entry, "###")}\n`;
1692      case "Junk":
1693        return serializeJunk(entry);
1694      default :
1695        throw new Error(`Unknown entry type: ${entry.type}`);
1696    }
1697  }
1698}
1699
1700
1701function serializeComment(comment, prefix = "#") {
1702  const prefixed = comment.content.split("\n").map(
1703    line => line.length ? `${prefix} ${line}` : prefix
1704  ).join("\n");
1705  // Add the trailing newline.
1706  return `${prefixed}\n`;
1707}
1708
1709
1710function serializeJunk(junk) {
1711  return junk.content;
1712}
1713
1714
1715function serializeMessage(message) {
1716  const parts = [];
1717
1718  if (message.comment) {
1719    parts.push(serializeComment(message.comment));
1720  }
1721
1722  parts.push(`${message.id.name} =`);
1723
1724  if (message.value) {
1725    parts.push(serializePattern(message.value));
1726  }
1727
1728  for (const attribute of message.attributes) {
1729    parts.push(serializeAttribute(attribute));
1730  }
1731
1732  parts.push("\n");
1733  return parts.join("");
1734}
1735
1736
1737function serializeTerm(term) {
1738  const parts = [];
1739
1740  if (term.comment) {
1741    parts.push(serializeComment(term.comment));
1742  }
1743
1744  parts.push(`-${term.id.name} =`);
1745  parts.push(serializePattern(term.value));
1746
1747  for (const attribute of term.attributes) {
1748    parts.push(serializeAttribute(attribute));
1749  }
1750
1751  parts.push("\n");
1752  return parts.join("");
1753}
1754
1755
1756function serializeAttribute(attribute) {
1757  const value = indent(serializePattern(attribute.value));
1758  return `\n    .${attribute.id.name} =${value}`;
1759}
1760
1761
1762function serializePattern(pattern) {
1763  const content = pattern.elements.map(serializeElement).join("");
1764  const startOnNewLine =
1765    pattern.elements.some(isSelectExpr) ||
1766    pattern.elements.some(includesNewLine);
1767
1768  if (startOnNewLine) {
1769    return `\n    ${indent(content)}`;
1770  }
1771
1772  return ` ${content}`;
1773}
1774
1775
1776function serializeElement(element) {
1777  switch (element.type) {
1778    case "TextElement":
1779      return element.value;
1780    case "Placeable":
1781      return serializePlaceable(element);
1782    default:
1783      throw new Error(`Unknown element type: ${element.type}`);
1784  }
1785}
1786
1787
1788function serializePlaceable(placeable) {
1789  const expr = placeable.expression;
1790  switch (expr.type) {
1791    case "Placeable":
1792      return `{${serializePlaceable(expr)}}`;
1793    case "SelectExpression":
1794      // Special-case select expression to control the whitespace around the
1795      // opening and the closing brace.
1796      return `{ ${serializeExpression(expr)}}`;
1797    default:
1798      return `{ ${serializeExpression(expr)} }`;
1799  }
1800}
1801
1802
1803function serializeExpression(expr) {
1804  switch (expr.type) {
1805    case "StringLiteral":
1806      return `"${expr.value}"`;
1807    case "NumberLiteral":
1808      return expr.value;
1809    case "VariableReference":
1810      return `$${expr.id.name}`;
1811    case "TermReference": {
1812      let out = `-${expr.id.name}`;
1813      if (expr.attribute) {
1814        out += `.${expr.attribute.name}`;
1815      }
1816      if (expr.arguments) {
1817        out += serializeCallArguments(expr.arguments);
1818      }
1819      return out;
1820    }
1821    case "MessageReference": {
1822      let out = expr.id.name;
1823      if (expr.attribute) {
1824        out += `.${expr.attribute.name}`;
1825      }
1826      return out;
1827    }
1828    case "FunctionReference":
1829      return `${expr.id.name}${serializeCallArguments(expr.arguments)}`;
1830    case "SelectExpression": {
1831      let out = `${serializeExpression(expr.selector)} ->`;
1832      for (let variant of expr.variants) {
1833        out += serializeVariant(variant);
1834      }
1835      return `${out}\n`;
1836    }
1837    case "Placeable":
1838      return serializePlaceable(expr);
1839    default:
1840      throw new Error(`Unknown expression type: ${expr.type}`);
1841  }
1842}
1843
1844
1845function serializeVariant(variant) {
1846  const key = serializeVariantKey(variant.key);
1847  const value = indent(serializePattern(variant.value));
1848
1849  if (variant.default) {
1850    return `\n   *[${key}]${value}`;
1851  }
1852
1853  return `\n    [${key}]${value}`;
1854}
1855
1856
1857function serializeCallArguments(expr) {
1858  const positional = expr.positional.map(serializeExpression).join(", ");
1859  const named = expr.named.map(serializeNamedArgument).join(", ");
1860  if (expr.positional.length > 0 && expr.named.length > 0) {
1861    return `(${positional}, ${named})`;
1862  }
1863  return `(${positional || named})`;
1864}
1865
1866
1867function serializeNamedArgument(arg) {
1868  const value = serializeExpression(arg.value);
1869  return `${arg.name.name}: ${value}`;
1870}
1871
1872
1873function serializeVariantKey(key) {
1874  switch (key.type) {
1875    case "Identifier":
1876      return key.name;
1877    case "NumberLiteral":
1878      return key.value;
1879    default:
1880      throw new Error(`Unknown variant key type: ${key.type}`);
1881  }
1882}
1883
1884/*
1885 * Abstract Visitor pattern
1886 */
1887class Visitor {
1888  visit(node) {
1889    if (Array.isArray(node)) {
1890      node.forEach(child => this.visit(child));
1891      return;
1892    }
1893    if (!(node instanceof BaseNode)) {
1894      return;
1895    }
1896    const visit = this[`visit${node.type}`] || this.genericVisit;
1897    visit.call(this, node);
1898  }
1899
1900  genericVisit(node) {
1901    for (const propname of Object.keys(node)) {
1902      this.visit(node[propname]);
1903    }
1904  }
1905}
1906
1907/*
1908 * Abstract Transformer pattern
1909 */
1910class Transformer extends Visitor {
1911  visit(node) {
1912    if (!(node instanceof BaseNode)) {
1913      return node;
1914    }
1915    const visit = this[`visit${node.type}`] || this.genericVisit;
1916    return visit.call(this, node);
1917  }
1918
1919  genericVisit(node) {
1920    for (const propname of Object.keys(node)) {
1921      const propvalue = node[propname];
1922      if (Array.isArray(propvalue)) {
1923        const newvals = propvalue
1924          .map(child => this.visit(child))
1925          .filter(newchild => newchild !== undefined);
1926        node[propname] = newvals;
1927      }
1928      if (propvalue instanceof BaseNode) {
1929        const new_val = this.visit(propvalue);
1930        if (new_val === undefined) {
1931          delete node[propname];
1932        } else {
1933          node[propname] = new_val;
1934        }
1935      }
1936    }
1937    return node;
1938  }
1939}
1940
1941const visitor = ({
1942  Visitor: Visitor,
1943  Transformer: Transformer
1944});
1945
1946/* eslint object-shorthand: "off",
1947          comma-dangle: "off",
1948          no-labels: "off" */
1949
1950this.EXPORTED_SYMBOLS = [
1951  ...Object.keys({
1952    FluentParser,
1953    FluentSerializer,
1954  }),
1955  ...Object.keys(ast),
1956  ...Object.keys(visitor),
1957];
1958