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