<commit_before>// ==========================================
// Copyright 2013 Twitter, Inc
// Licensed under The MIT License
// http://opensource.org/licenses/MIT
// ==========================================

define(

  [],

  function() {
    'use strict';

    // ==========================================
    // Search object model
    // ==========================================

    function traverse(util, searchTerm, options) {
      options = options || {};
      var obj = options.obj || window;
      var path = options.path || ((obj==window) ? 'window' : '');
      var props = Object.keys(obj);
      props.forEach(function(prop) {
        if ((tests[util] || util)(searchTerm, obj, prop)){
          console.log([path, '.', prop].join(''), '->', ['(', typeof obj[prop], ')'].join(''), obj[prop]);
        }
        if (Object.prototype.toString.call(obj[prop]) == '[object Object]' && (obj[prop] != obj) && path.split('.').indexOf(prop) == -1) {
          traverse(util, searchTerm, {obj: obj[prop], path: [path,prop].join('.')});
        }
      });
    }

    function search(util, expected, searchTerm, options) {
      if (!expected || typeof searchTerm == expected) {
        traverse(util, searchTerm, options);
      } else {
        console.error([searchTerm, 'must be', expected].join(' '));
      }
    }

    var tests = {
      'name': function(searchTerm, obj, prop) {return searchTerm == prop;},
      'nameContains': function(searchTerm, obj, prop) {return prop.indexOf(searchTerm) > -1;},
      'type': function(searchTerm, obj, prop) {return obj[prop] instanceof searchTerm;},
      'value': function(searchTerm, obj, prop) {return obj[prop] === searchTerm;},
      'valueCoerced': function(searchTerm, obj, prop) {return obj[prop] == searchTerm;}
    };

    function byName(searchTerm, options) {search('name', 'string', searchTerm, options);}
    function byNameContains(searchTerm, options) {search('nameContains', 'string', searchTerm, options);}
    function byType(searchTerm, options) {search('type', 'function', searchTerm, options);}
    function byValue(searchTerm, options) {search('value', null, searchTerm, options);}
    function byValueCoerced(searchTerm, options) {search('valueCoerced', null, searchTerm, options);}
    function custom(fn, options) {traverse(fn, null, options);}

    // ==========================================
    // Event logging
    // ==========================================

    var ALL = 'all'; //no filter

    //no logging by default
    var defaultEventNamesFilter = [];
    var defaultActionsFilter = [];

    var logFilter = retrieveLogFilter();

    function filterEventLogsByAction(/*actions*/) {
      var actions = [].slice.call(arguments);

      logFilter.eventNames.length || (logFilter.eventNames = ALL);
      logFilter.actions = actions.length ? actions : ALL;
      saveLogFilter();
    }

    function filterEventLogsByName(/*eventNames*/) {
      var eventNames = [].slice.call(arguments);

      logFilter.actions.length || (logFilter.actions = ALL);
      logFilter.eventNames = eventNames.length ? eventNames : ALL;
      saveLogFilter();
    }

    function hideAllEventLogs() {
      logFilter.actions = [];
      logFilter.eventNames = [];
      saveLogFilter();
    }

    function showAllEventLogs() {
      logFilter.actions = ALL;
      logFilter.eventNames = ALL;
      saveLogFilter();
    }

    function saveLogFilter() {
      if (window.localStorage) {
        localStorage.setItem('logFilter_eventNames', logFilter.eventNames);
        localStorage.setItem('logFilter_actions', logFilter.actions);
      }
    }

    function retrieveLogFilter() {
      var result = {
        eventNames: (window.localStorage && localStorage.getItem('logFilter_eventNames')) || defaultEventNamesFilter,
        actions: (window.localStorage && localStorage.getItem('logFilter_actions')) || defaultActionsFilter
      };

      // reconstitute arrays
      Object.keys(result).forEach(function(k) {
        var thisProp = result[k];
        if (typeof thisProp == 'string' && thisProp !== ALL) {
          result[k] = thisProp.split(',');
        }
      });
      return result;
    }

    return {

      enable: function(enable) {
        this.enabled = !!enable;

        if (enable && window.console) {
          console.info('Booting in DEBUG mode');
          console.info('You can configure event logging with DEBUG.events.logAll()/logNone()/logByName()/logByAction()');
        }

        window.DEBUG = this;
      },

      find: {
        byName: byName,
        byNameContains: byNameContains,
        byType: byType,
        byValue: byValue,
        byValueCoerced: byValueCoerced,
        custom: custom
      },

      events: {
        logFilter: logFilter,

        // Accepts any number of action args
        // e.g. DEBUG.events.logByAction("on", "off")
        logByAction: filterEventLogsByAction,

        // Accepts any number of event name args (inc. regex or wildcards)
        // e.g. DEBUG.events.logByName(/ui.*/, "*Thread*");
        logByName: filterEventLogsByName,

        logAll: showAllEventLogs,
        logNone: hideAllEventLogs
      }
    };
  }
);
<commit_msg>correctly catching exceptions around window.localStorage calls, otherwise window.localStorage calls can crash iframed applications using flight<commit_after>// ==========================================
// Copyright 2013 Twitter, Inc
// Licensed under The MIT License
// http://opensource.org/licenses/MIT
// ==========================================

define(

  [],

  function() {
    'use strict';

    // ==========================================
    // Search object model
    // ==========================================

    function traverse(util, searchTerm, options) {
      options = options || {};
      var obj = options.obj || window;
      var path = options.path || ((obj==window) ? 'window' : '');
      var props = Object.keys(obj);
      props.forEach(function(prop) {
        if ((tests[util] || util)(searchTerm, obj, prop)){
          console.log([path, '.', prop].join(''), '->', ['(', typeof obj[prop], ')'].join(''), obj[prop]);
        }
        if (Object.prototype.toString.call(obj[prop]) == '[object Object]' && (obj[prop] != obj) && path.split('.').indexOf(prop) == -1) {
          traverse(util, searchTerm, {obj: obj[prop], path: [path,prop].join('.')});
        }
      });
    }

    function search(util, expected, searchTerm, options) {
      if (!expected || typeof searchTerm == expected) {
        traverse(util, searchTerm, options);
      } else {
        console.error([searchTerm, 'must be', expected].join(' '));
      }
    }

    var tests = {
      'name': function(searchTerm, obj, prop) {return searchTerm == prop;},
      'nameContains': function(searchTerm, obj, prop) {return prop.indexOf(searchTerm) > -1;},
      'type': function(searchTerm, obj, prop) {return obj[prop] instanceof searchTerm;},
      'value': function(searchTerm, obj, prop) {return obj[prop] === searchTerm;},
      'valueCoerced': function(searchTerm, obj, prop) {return obj[prop] == searchTerm;}
    };

    function byName(searchTerm, options) {search('name', 'string', searchTerm, options);}
    function byNameContains(searchTerm, options) {search('nameContains', 'string', searchTerm, options);}
    function byType(searchTerm, options) {search('type', 'function', searchTerm, options);}
    function byValue(searchTerm, options) {search('value', null, searchTerm, options);}
    function byValueCoerced(searchTerm, options) {search('valueCoerced', null, searchTerm, options);}
    function custom(fn, options) {traverse(fn, null, options);}

    // ==========================================
    // Event logging
    // ==========================================

    var ALL = 'all'; //no filter

    //no logging by default
    var defaultEventNamesFilter = [];
    var defaultActionsFilter = [];

    var logFilter = retrieveLogFilter();

    function filterEventLogsByAction(/*actions*/) {
      var actions = [].slice.call(arguments);

      logFilter.eventNames.length || (logFilter.eventNames = ALL);
      logFilter.actions = actions.length ? actions : ALL;
      saveLogFilter();
    }

    function filterEventLogsByName(/*eventNames*/) {
      var eventNames = [].slice.call(arguments);

      logFilter.actions.length || (logFilter.actions = ALL);
      logFilter.eventNames = eventNames.length ? eventNames : ALL;
      saveLogFilter();
    }

    function hideAllEventLogs() {
      logFilter.actions = [];
      logFilter.eventNames = [];
      saveLogFilter();
    }

    function showAllEventLogs() {
      logFilter.actions = ALL;
      logFilter.eventNames = ALL;
      saveLogFilter();
    }

    function saveLogFilter() {
      try {
        if (window.localStorage) {
          localStorage.setItem('logFilter_eventNames', logFilter.eventNames);
          localStorage.setItem('logFilter_actions', logFilter.actions);
        }
      } catch (ignored) {};
    }

    function retrieveLogFilter() {
      var eventNames, actions;
      try {
        eventNames = (window.localStorage && localStorage.getItem('logFilter_eventNames'));
        actions = (window.localStorage && localStorage.getItem('logFilter_actions'));
      } catch(ignored) {
        eventNames = defaultEventNamesFilter;
        actions = defaultActionsFilter;
      }
      var result = {
        eventNames: eventNames,
        actions: actions
      };

      // reconstitute arrays
      Object.keys(result).forEach(function(k) {
        var thisProp = result[k];
        if (typeof thisProp == 'string' && thisProp !== ALL) {
          result[k] = thisProp.split(',');
        }
      });
      return result;
    }

    return {

      enable: function(enable) {
        this.enabled = !!enable;

        if (enable && window.console) {
          console.info('Booting in DEBUG mode');
          console.info('You can configure event logging with DEBUG.events.logAll()/logNone()/logByName()/logByAction()');
        }

        window.DEBUG = this;
      },

      find: {
        byName: byName,
        byNameContains: byNameContains,
        byType: byType,
        byValue: byValue,
        byValueCoerced: byValueCoerced,
        custom: custom
      },

      events: {
        logFilter: logFilter,

        // Accepts any number of action args
        // e.g. DEBUG.events.logByAction("on", "off")
        logByAction: filterEventLogsByAction,

        // Accepts any number of event name args (inc. regex or wildcards)
        // e.g. DEBUG.events.logByName(/ui.*/, "*Thread*");
        logByName: filterEventLogsByName,

        logAll: showAllEventLogs,
        logNone: hideAllEventLogs
      }
    };
  }
);
<|endoftext|>


<commit_before>define(function () {
  return function() {
    this.render = function (d) {
      var el = document.createElement("div")
      d.appendChild(el)
      var s = "
Über meshviewer
"

      s += "
Mit Doppelklick und Shift+Doppelklick kann man in der Karte "
      s += "auch zoomen.
"

      s += "
AGPL 3
"

      s += "
Copyright (C) Nils Schneider
"

      s += "
This program is free software: you can redistribute it and/or "
      s += "modify it under the terms of the GNU Affero General Public "
      s += "License as published by the Free Software Foundation, either "
      s += "version 3 of the License, or (at your option) any later version.
"

      s += "
This program is distributed in the hope that it will be useful, "
      s += "but WITHOUT ANY WARRANTY; without even the implied warranty of "
      s += "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the "
      s += "GNU Affero General Public License for more details.
"

      s += "
You should have received a copy of the GNU Affero General "
      s += "Public License along with this program. If not, see "
      s += ""
      s += "https://www.gnu.org/licenses/.
"

      s += "
You may find the source code at "
      s += ""
      s += "https://github.com/tcatm/meshviewer."

      el.innerHTML = s
    }
  }
})
<commit_msg>Fix copyright notice & source code location in about tab<commit_after>define(function () {
  return function() {
    this.render = function (d) {
      var el = document.createElement("div")
      d.appendChild(el)
      var s = "
Über meshviewer
"

      s += "
Mit Doppelklick und Shift+Doppelklick kann man in der Karte "
      s += "auch zoomen.
"

      s += "
AGPL 3
"

      s += "
Copyright (C) Nils Schneider & "
      s += ""
      s += "others
"

      s += "
This program is free software: you can redistribute it and/or "
      s += "modify it under the terms of the GNU Affero General Public "
      s += "License as published by the Free Software Foundation, either "
      s += "version 3 of the License, or (at your option) any later version.
"

      s += "
This program is distributed in the hope that it will be useful, "
      s += "but WITHOUT ANY WARRANTY; without even the implied warranty of "
      s += "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the "
      s += "GNU Affero General Public License for more details.
"

      s += "
You should have received a copy of the GNU Affero General "
      s += "Public License along with this program. If not, see "
      s += ""
      s += "https://www.gnu.org/licenses/.
"

      s += "
You may find the original source code at "
      s += ""
      s += "https://github.com/tcatm/meshviewer.
"

      s += "
The current source code with modifications for Freifunk München "
      s += "can be found at "
      s += "https://github.com/freifunkMUC/meshviewer.
"

      el.innerHTML = s
    }
  }
})
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { ApiRequest } from '../../services/ApiRequest';
import Routes from '../../services/Routes';
import TopBar from '../TopBar/TopBar';
import NavigationMenu from '../NavigationMenu';
import pages from '../../constants/pages';
import DateFormatter from '../../utils/DateFormatter';
import MealPlanner from './MealPlanner';
import ConfirmModal from '../base/modal/ConfirmModal';
import PikadayWrapper from '../../lib-components/PikadayWrapper';

export default class PlannerPageContainer extends Component {
    constructor(props) {
        super(props);

        this.state = {
            message: null,
            arePlannedDaysVisible: false,
            plannedDays: [],
            mealToBeRemoved: null
        };

        this.onRemoveMeal = this.onRemoveMeal.bind(this);
    }

    componentDidMount() {
        const selectedDate = this.parseSelectedDate(this.getDateFromUrl());
        this.retrievePlannedDays(selectedDate);

        if (!this.getDateFromUrl()) {
            this.props.router.push(Routes.mealPlannerWithDate(this.getSelectedDateAsString()));
        }
    }

    componentWillReceiveProps(nextProps) {
        const newDate = nextProps.location.query.date;
        const isDayInState = this.state.plannedDays.some(day => day.mealDate === newDate);

        if (!isDayInState) {
            const selectedDate = this.parseSelectedDate(newDate);
            this.retrievePlannedDays(selectedDate);
        }
    }

    getLoadingMessage() {
        return (
 Loading your planner...
);
    }

    getRemovingYourRecipeMessage() {
        return (
 Removing your recipe...
);
    }

    getErrorMessage() {
        return (

 There was an error with our server. Please refresh the page!

        );
    }

    renderPlanner() {
        if (!this.state.arePlannedDaysVisible) {
            return null;
        }

        return (
            <MealPlanner
                selectedDay={this.getSelectedDateAsString()}
                days={this.state.plannedDays}
                onDayChange={mealDate => this.props.router.push(Routes.mealPlannerWithDate(mealDate))}
                onRemoveMeal={this.onRemoveMeal} />
        );
    }

    getDateFromUrl() {
        return this.props.location.query.date;
    }

    parseSelectedDate(dateToParse) {
        let date = DateFormatter.stringToDate(dateToParse);
        return date.isValid() ? date : DateFormatter.getLocalCurrentDate();
    }

    getSelectedDateAsString() {
        const date = this.parseSelectedDate(this.getDateFromUrl());
        return DateFormatter.dateToString(date);
    }

    retrievePlannedDays(selectedDate) {
        this.setState({ message: this.getLoadingMessage(), arePlannedDaysVisible: false });

        const { startOfWeek, endOfWeek } = DateFormatter.extractLocaleStartAndEndOfWeek(selectedDate);

        const startOfWeekUtc = DateFormatter.markLocaleDateAsUtc(startOfWeek);
        const endOfWeekUtc = DateFormatter.markLocaleDateAsUtc(endOfWeek);

        const startOfWeekIsoString = DateFormatter.dateToIsoString(startOfWeekUtc);
        const endOfWeekIsoString = DateFormatter.dateToIsoString(endOfWeekUtc);

        ApiRequest.retrieveMeals(startOfWeekIsoString, endOfWeekIsoString)
            .then(rsp => {
                const days = rsp.body;
                const plannedDays = days.map(day => ({ ...day, mealDate: DateFormatter.isoStringToDateString(day.mealDate) }));
                this.setState({ message: null, arePlannedDaysVisible: true, plannedDays });
            }, err => {
                this.setState({ message: this.getErrorMessage() });
            });
    }

    onRemoveMeal(mealId, recipeName) {
        this.setState({ mealToBeRemoved: { mealId, recipeName } })
    }

    removeMeal() {
        const { mealId } = this.state.mealToBeRemoved;

        const plannedDays = this.state.plannedDays.map(day => (
            { ...day, meals: day.meals.filter(meal => meal.id !== mealId) }
        ));

        // optimistic design (do not wait for the request to finish)
        this.setState({ mealToBeRemoved: null, plannedDays });

        ApiRequest.removeMeal(mealId).then(response => {
            const { isSuccess, message } = response.body;
            if (!isSuccess) {
                console.log(message);
            }
        }, err => { console.log(err); });
    }

    renderRemoveMealModal() {
        if (!this.state.mealToBeRemoved) {
            return null;
        }

        const modalTitle = `Are you sure you want to remove '${this.state.mealToBeRemoved.recipeName}' from planner?`;

        return (
            <ConfirmModal
                isOpen={true}
                title={modalTitle}
                cancelButtonText="Cancel"
                onCancel={() => this.setState({ mealToBeRemoved: null })}
                actionButtonText="Remove"
                onAction={() => this.removeMeal()}
            />
        );
    }

    renderDatePicker() {
        return (

  Choose a date:

                <PikadayWrapper
                    className="pes-meal-planner-container__datepicker"
                    onSelect={(date) => console.log(date)} />

        );
    }

    render() {
        return (

                <TopBar addButton addButtonOnClick={() => this.props.router.push(Routes.addMeal(this.getSelectedDateAsString(), 1000))} />

                    {this.renderDatePicker()}
                    {this.state.message}
                    {this.renderPlanner()}
                    {this.renderRemoveMealModal()}

                <NavigationMenu activeItem={pages.PLANNER} />

        );
    }
}<commit_msg>Link date picker date with url parameter of date<commit_after>import React, { Component } from 'react';
import { ApiRequest } from '../../services/ApiRequest';
import Routes from '../../services/Routes';
import TopBar from '../TopBar/TopBar';
import NavigationMenu from '../NavigationMenu';
import pages from '../../constants/pages';
import DateFormatter from '../../utils/DateFormatter';
import MealPlanner from './MealPlanner';
import ConfirmModal from '../base/modal/ConfirmModal';
import PikadayWrapper from '../../lib-components/PikadayWrapper';
import moment from 'moment';

export default class PlannerPageContainer extends Component {
    constructor(props) {
        super(props);

        this.state = {
            message: null,
            arePlannedDaysVisible: false,
            plannedDays: [],
            mealToBeRemoved: null
        };

        this.onRemoveMeal = this.onRemoveMeal.bind(this);
    }

    componentDidMount() {
        const selectedDate = this.parseSelectedDate(this.getDateFromUrl());
        this.retrievePlannedDays(selectedDate);

        if (!this.getDateFromUrl()) {
            this.props.router.push(Routes.mealPlannerWithDate(this.getSelectedDateAsString()));
        }
    }

    componentWillReceiveProps(nextProps) {
        const newDate = nextProps.location.query.date;
        const isDayInState = this.state.plannedDays.some(day => day.mealDate === newDate);

        if (!isDayInState) {
            const selectedDate = this.parseSelectedDate(newDate);
            this.retrievePlannedDays(selectedDate);
        }
    }

    getLoadingMessage() {
        return (
 Loading your planner...
);
    }

    getRemovingYourRecipeMessage() {
        return (
 Removing your recipe...
);
    }

    getErrorMessage() {
        return (

 There was an error with our server. Please refresh the page!

        );
    }

    renderPlanner() {
        if (!this.state.arePlannedDaysVisible) {
            return null;
        }

        return (
            <MealPlanner
                selectedDay={this.getSelectedDateAsString()}
                days={this.state.plannedDays}
                onDayChange={mealDate => this.props.router.push(Routes.mealPlannerWithDate(mealDate))}
                onRemoveMeal={this.onRemoveMeal} />
        );
    }

    getDateFromUrl() {
        return this.props.location.query.date;
    }

    parseSelectedDate(dateToParse) {
        let date = DateFormatter.stringToDate(dateToParse);
        return date.isValid() ? date : DateFormatter.getLocalCurrentDate();
    }

    getSelectedDateAsString() {
        const date = this.parseSelectedDate(this.getDateFromUrl());
        return DateFormatter.dateToString(date);
    }

    retrievePlannedDays(selectedDate) {
        this.setState({ message: this.getLoadingMessage(), arePlannedDaysVisible: false });

        const { startOfWeek, endOfWeek } = DateFormatter.extractLocaleStartAndEndOfWeek(selectedDate);

        const startOfWeekUtc = DateFormatter.markLocaleDateAsUtc(startOfWeek);
        const endOfWeekUtc = DateFormatter.markLocaleDateAsUtc(endOfWeek);

        const startOfWeekIsoString = DateFormatter.dateToIsoString(startOfWeekUtc);
        const endOfWeekIsoString = DateFormatter.dateToIsoString(endOfWeekUtc);

        ApiRequest.retrieveMeals(startOfWeekIsoString, endOfWeekIsoString)
            .then(rsp => {
                const days = rsp.body;
                const plannedDays = days.map(day => ({ ...day, mealDate: DateFormatter.isoStringToDateString(day.mealDate) }));
                this.setState({ message: null, arePlannedDaysVisible: true, plannedDays });
            }, err => {
                this.setState({ message: this.getErrorMessage() });
            });
    }

    onRemoveMeal(mealId, recipeName) {
        this.setState({ mealToBeRemoved: { mealId, recipeName } })
    }

    removeMeal() {
        const { mealId } = this.state.mealToBeRemoved;

        const plannedDays = this.state.plannedDays.map(day => (
            { ...day, meals: day.meals.filter(meal => meal.id !== mealId) }
        ));

        // optimistic design (do not wait for the request to finish)
        this.setState({ mealToBeRemoved: null, plannedDays });

        ApiRequest.removeMeal(mealId).then(response => {
            const { isSuccess, message } = response.body;
            if (!isSuccess) {
                console.log(message);
            }
        }, err => { console.log(err); });
    }

    renderRemoveMealModal() {
        if (!this.state.mealToBeRemoved) {
            return null;
        }

        const modalTitle = `Are you sure you want to remove '${this.state.mealToBeRemoved.recipeName}' from planner?`;

        return (
            <ConfirmModal
                isOpen={true}
                title={modalTitle}
                cancelButtonText="Cancel"
                onCancel={() => this.setState({ mealToBeRemoved: null })}
                actionButtonText="Remove"
                onAction={() => this.removeMeal()}
            />
        );
    }

    renderDatePicker() {
        return (

  Choose a date:

                <PikadayWrapper
                    className="pes-meal-planner-container__datepicker"
                    onSelect={(date) => {
                        const dateString = DateFormatter.dateToString(moment(date));
                        this.props.router.push(Routes.mealPlannerWithDate(dateString));
                    }} />

        );
    }

    render() {
        return (

                <TopBar addButton addButtonOnClick={() => this.props.router.push(Routes.addMeal(this.getSelectedDateAsString(), 1000))} />

                    {this.renderDatePicker()}
                    {this.state.message}
                    {this.renderPlanner()}
                    {this.renderRemoveMealModal()}

                <NavigationMenu activeItem={pages.PLANNER} />

        );
    }
}<|endoftext|>


<commit_before>import { L10nError } from './errors';
import { resolve, ask, fail } from './readwrite';
import builtins, {
  FTLNone, FTLNumber, FTLDateTime, FTLKeyword, FTLList
} from './builtins';

// Unicode bidi isolation characters
const FSI = '\u2068';
const PDI = '\u2069';

const MAX_PLACEABLE_LENGTH = 2500;

function* mapValues(arr) {
  let values = new FTLList();
  for (let elem of arr) {
    values.push(yield* Value(elem));
  }
  return values;
}

function err(msg, fallback) {
  return fail(new FTLNone(fallback), new L10nError(msg));
}

// Helper for choosing entity value
function* DefaultMember(members) {
  for (let member of members) {
    if (member.def) {
      return yield member;
    }
  }

  return yield err('No default');
}

// Half-resolved expressions

function* Expression(expr) {
  switch (expr.type) {
    case 'ref':
      return yield* EntityReference(expr);
    case 'blt':
      return yield* BuiltinReference(expr);
    case 'mem':
      return yield* MemberExpression(expr);
    case 'sel':
      return yield* SelectExpression(expr);
    default:
      return yield expr;
  }
}

function* EntityReference(expr) {
  const rc = yield ask();
  const entity = rc.ctx._getEntity(rc.lang, expr.name);

  if (!entity) {
    return yield err(`Unknown entity: ${expr.name}`, expr.name);
  }

  return yield entity;
}

function* BuiltinReference(expr) {
  const builtin = builtins[expr.name];

  if (!builtin) {
    return yield err(`Unknown built-in: ${expr.name}()`, `${expr.name}()`);
  }

  return yield builtin;
}

function* MemberExpression(expr) {
  const entity = yield* Expression(expr.obj);
  const key = yield* Value(expr.key);
  const rc = yield ask();

  for (let member of entity.traits) {
    const memberKey = yield* Value(member.key);
    if (key.match(rc, memberKey)) {
      return yield member;
    }
  }

  return yield err(`Unknown trait: ${key.toString(rc)}`, entity);
}

function* SelectExpression(expr) {
  const selector = yield* Value(expr.exp);
  if (selector instanceof FTLNone) {
    return yield* DefaultMember(expr.vars);
  }

  for (let variant of expr.vars) {
    const key = yield* Value(variant.key);

    if (key instanceof FTLNumber &&
        selector instanceof FTLNumber &&
        key.valueOf() === selector.valueOf()) {
      return yield variant;
    }

    const rc = yield ask();

    if (key instanceof FTLKeyword &&
        key.match(rc, selector)) {
      return yield variant;
    }
  }

  return yield* DefaultMember(expr.vars);
}

// Fully-resolved expressions

function* Value(expr) {
  if (typeof expr === 'string' || expr === null) {
    return yield expr;
  }

  if (Array.isArray(expr)) {
    return yield* Pattern(expr);
  }

  const node = yield* Expression(expr);
  if (node instanceof FTLNone) {
    // Expression short-circuited into a simple string or a fallback
    return yield* Value(node);
  }

  switch (node.type) {
    case 'kw':
      return yield new FTLKeyword(node);
    case 'num':
      return yield new FTLNumber(node.val);
    case 'ext':
      return yield* ExternalArgument(node);
    case 'call':
      return yield* CallExpression(expr);
    default:
      return node.key ? // is it a Member?
        yield* Value(node.val) :
        yield* Entity(node);
  }
}

function* ExternalArgument(expr) {
  const name = expr.name;
  const { args } = yield ask();

  if (!args || !args.hasOwnProperty(name)) {
    return yield err(`Unknown external: ${name}`, name);
  }

  const arg = args[name];

  switch (typeof arg) {
    case 'string':
      return yield arg;
    case 'number':
      return yield new FTLNumber(arg);
    case 'object':
      if (Array.isArray(arg)) {
        return yield* mapValues(arg);
      }
      if (arg instanceof Date) {
        return yield new FTLDateTime(arg);
      }
    default:
      return yield err(
        'Unsupported external type: ' + name + ', ' + typeof arg, name
      );
  }
}

function* CallExpression(expr) {
  const callee = yield* Expression(expr.name);

  if (callee instanceof FTLNone) {
    return callee;
  }

  let pargs = [];
  let kargs = [];

  for (let arg of expr.args) {
    if (arg.type === 'kv') {
      const val = yield* Value(arg.val);
      kargs[arg.name] = val;
    } else {
      const val = yield* Value(arg);
      pargs.push(val);
    }
  }

  return yield callee(pargs, kargs);
}

function* Pattern(ptn) {
  const rc = yield ask();

  if (rc.dirty.has(ptn)) {
    return yield err('Cyclic reference');
  }

  rc.dirty.add(ptn);
  let result = '';

  for (let part of ptn) {
    if (typeof part === 'string') {
      result += part;
    } else {
      const value = part.length === 1 ?
        yield* Value(part[0]) : yield* mapValues(part);

      const str = value.toString(rc);
      if (str.length > MAX_PLACEABLE_LENGTH) {
        const trimmed = yield err(
          'Too many characters in placeable ' +
          `(${str.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`,
          str.substr(0, MAX_PLACEABLE_LENGTH)
        );
        result += trimmed;
      } else {
        result += FSI + str + PDI;
      }
    }
  }

  rc.dirty.delete(ptn);
  return yield result;
}

function* Entity(entity) {
  if (!entity.traits) {
    return yield* Value(entity);
  }

  if (entity.val !== undefined) {
    return yield* Value(entity.val);
  }

  const def = yield* DefaultMember(entity.traits);
  return yield* Value(def);
}

function* valueOf(entity) {
  return yield* Value(entity);
}

export function format(ctx, lang, args, entity) {
  if (typeof entity === 'string') {
    return [entity, []];
  }

  return resolve(valueOf(entity)).run({
    ctx, lang, args, dirty: new WeakSet()
  });
}
<commit_msg>Simplify Expressions and Values<commit_after>import { L10nError } from './errors';
import { resolve, ask, fail } from './readwrite';
import builtins, {
  FTLNone, FTLNumber, FTLDateTime, FTLKeyword, FTLList
} from './builtins';

// Unicode bidi isolation characters
const FSI = '\u2068';
const PDI = '\u2069';

const MAX_PLACEABLE_LENGTH = 2500;

function* mapValues(arr) {
  let values = new FTLList();
  for (let elem of arr) {
    values.push(yield* Value(elem));
  }
  return values;
}

function err(msg, fallback) {
  return fail(new FTLNone(fallback), new L10nError(msg));
}

// Helper for choosing entity value
function* DefaultMember(members) {
  for (let member of members) {
    if (member.def) {
      return yield member;
    }
  }

  return yield err('No default');
}

// Half-resolved expressions

function* EntityReference(expr) {
  const rc = yield ask();
  const entity = rc.ctx._getEntity(rc.lang, expr.name);

  if (!entity) {
    return yield err(`Unknown entity: ${expr.name}`, expr.name);
  }

  return yield entity;
}

function* BuiltinReference(expr) {
  const builtin = builtins[expr.name];

  if (!builtin) {
    return yield err(`Unknown built-in: ${expr.name}()`, `${expr.name}()`);
  }

  return yield builtin;
}

function* MemberExpression(expr) {
  const entity = yield* EntityReference(expr.obj);
  const key = yield* Value(expr.key);
  const rc = yield ask();

  for (let member of entity.traits) {
    const memberKey = yield* Value(member.key);
    if (key.match(rc, memberKey)) {
      return yield member;
    }
  }

  return yield err(`Unknown trait: ${key.toString(rc)}`, entity);
}

function* SelectExpression(expr) {
  const selector = yield* Value(expr.exp);
  if (selector instanceof FTLNone) {
    return yield* DefaultMember(expr.vars);
  }

  for (let variant of expr.vars) {
    const key = yield* Value(variant.key);

    if (key instanceof FTLNumber &&
        selector instanceof FTLNumber &&
        key.valueOf() === selector.valueOf()) {
      return yield variant;
    }

    const rc = yield ask();

    if (key instanceof FTLKeyword &&
        key.match(rc, selector)) {
      return yield variant;
    }
  }

  return yield* DefaultMember(expr.vars);
}

// Fully-resolved expressions

function* Value(expr) {
  if (typeof expr === 'string' || expr === null) {
    return yield expr;
  }

  if (Array.isArray(expr)) {
    return yield* Pattern(expr);
  }

  switch (expr.type) {
    case 'kw':
      return yield new FTLKeyword(expr);
    case 'num':
      return yield new FTLNumber(expr.val);
    case 'ext':
      return yield* ExternalArgument(expr);
    case 'call':
      return yield* CallExpression(expr);
    case 'ref':
      const ref = yield* EntityReference(expr);
      return yield* Entity(ref);
    case 'blt':
      const blt = yield* BuiltinReference(expr);
      return yield* Value(blt.val);
    case 'mem':
      const mem = yield* MemberExpression(expr);
      return yield* Value(mem.val);
    case 'sel':
      const sel = yield* SelectExpression(expr);
      return yield* Value(sel.val);
    default:
      return yield* Value(expr.val);
  }
}

function* ExternalArgument(expr) {
  const name = expr.name;
  const { args } = yield ask();

  if (!args || !args.hasOwnProperty(name)) {
    return yield err(`Unknown external: ${name}`, name);
  }

  const arg = args[name];

  switch (typeof arg) {
    case 'string':
      return yield arg;
    case 'number':
      return yield new FTLNumber(arg);
    case 'object':
      if (Array.isArray(arg)) {
        return yield* mapValues(arg);
      }
      if (arg instanceof Date) {
        return yield new FTLDateTime(arg);
      }
    default:
      return yield err(
        'Unsupported external type: ' + name + ', ' + typeof arg, name
      );
  }
}

function* CallExpression(expr) {
  const callee = yield* BuiltinReference(expr.name);

  if (callee instanceof FTLNone) {
    return callee;
  }

  let pargs = [];
  let kargs = [];

  for (let arg of expr.args) {
    if (arg.type === 'kv') {
      const val = yield* Value(arg.val);
      kargs[arg.name] = val;
    } else {
      const val = yield* Value(arg);
      pargs.push(val);
    }
  }

  return yield callee(pargs, kargs);
}

function* Pattern(ptn) {
  const rc = yield ask();

  if (rc.dirty.has(ptn)) {
    return yield err('Cyclic reference');
  }

  rc.dirty.add(ptn);
  let result = '';

  for (let part of ptn) {
    if (typeof part === 'string') {
      result += part;
    } else {
      const value = part.length === 1 ?
        yield* Value(part[0]) : yield* mapValues(part);

      const str = value.toString(rc);
      if (str.length > MAX_PLACEABLE_LENGTH) {
        const trimmed = yield err(
          'Too many characters in placeable ' +
          `(${str.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`,
          str.substr(0, MAX_PLACEABLE_LENGTH)
        );
        result += trimmed;
      } else {
        result += FSI + str + PDI;
      }
    }
  }

  rc.dirty.delete(ptn);
  return yield result;
}

function* Entity(entity) {
  if (entity.val !== undefined) {
    return yield* Value(entity.val);
  }

  if (!entity.traits) {
    return yield* Value(entity);
  }

  const def = yield* DefaultMember(entity.traits);
  return yield* Value(def);
}

function* valueOf(entity) {
  return yield* Entity(entity);
}

export function format(ctx, lang, args, entity) {
  if (typeof entity === 'string') {
    return [entity, []];
  }

  return resolve(valueOf(entity)).run({
    ctx, lang, args, dirty: new WeakSet()
  });
}
<|endoftext|>


<commit_before>var Model = require('./Model');
var triggerParams = require('./mixins/triggerParams');
var volumeParams = require('./mixins/volumeParams');
var connectable = require('./mixins/connectable');
var bypassable = require('./mixins/bypassable');
var Params = require('./Params');

var context = null;

function createPool (factoryName) {
  var nodes = [];
  return function pool(node) {
    if (node) {
      nodes.push(node);
    }
    else if (nodes.length) {
      return nodes.shift();
    }
    else {
      return context[factoryName]();
    }
  };
}

var gainPool = createPool('createGain');
var pannerPool = createPool('createPanner');

var destination = null;

var Layer = Model.extend(triggerParams, volumeParams, {

  type: 'layer',

  props: {
    sources: ['object', true, function () { return {}; }]
  },

  start: function (time, note, channel, pattern) {
    if (this.mute) { return; }

    if (typeof time !== 'number') {
      pattern = channel;
      channel = note;
      note = time;
      time = this.context.currentTime;
    }
    note = note || {};

    context = context || this.context;

    var params = this._params(note, channel, pattern);
    var source = this._source(params);

    if (source) {
      var connections = this._getConnections(note, channel, pattern);
      var destination = this._connectDestination(connections, params);
      var gain = source._gain = gainPool();
      this._configureAttack(time, params, gain, destination);
      var panner = this._configurePan(params, gain);
      source.connect(panner || gain);

      this._startSource(time, params, source);

      var cid = note.cid || 'null';
      this.sources[cid] = this.sources[cid] || [];
      this.sources[cid].push(source);

      if (params.length) {
        var stopTime = time + params.length;
        this.stop(stopTime, note, channel, pattern);
      }

      source.onended = function () {
        var index = this.sources[cid].indexOf(source);
        this.sources[cid].splice(index, 1);

        gain.disconnect();
        source.disconnect();

        if (panner) {
          panner.disconnect();
          pannerPool(panner);
        }

        gainPool(gain);

        source = source.onended = source._gain = gain = panner = null;
        if (typeof note.after === 'function') {
          note.after();
        }
      }.bind(this);
    }
  },

  stop: function (time, note, channel, pattern) {
    if (typeof time !== 'number') {
      pattern = channel;
      channel = note;
      note = time;
      time = this.context.currentTime;
    }
    note = note || {};

    var params = this._params(note, channel, pattern);
    var cid = note.cid || 'null';
    var sources = this.sources[cid] || [];

    if (cid === 'null') {
      sources = [];
      Object.keys(this.sources).forEach(function (cid) {
        sources = sources.concat(this.sources[cid]);
      }.bind(this));
    }

    sources.forEach(function (source) {
      if (source._gain) {
        this._configureRelease(time - params.release, params, source._gain);
      }
      this._stopSource(time, params, source);
    }.bind(this));
  },

  _paramsSources: function (note, channel, pattern) {
    var slot = this.collection && this.collection.parent || {};
    var kit = slot && slot.kit || {};
    return [note, channel, { volume: pattern.volume }, this, slot, kit];
  },

  _params: function (note, channel, pattern) {
    var sources = this._paramsSources(note, channel, pattern);
    return Params.fromSources(sources);
  },

  _getConnections: function (note, channel, pattern) {
    var sources = this._paramsSources(note, channel, pattern);
    var connections = [];
    sources.forEach(function (source) {
      if (source && source.connections) {
        connections = connections.concat(source.connections);
      }
    });
    return connections;
  },

  _createGain: function (params, source) {
    return this.context.createGain();
  },

  _startSource: function (time, params, source) {
    source.start(time);
  },

  _stopSource: function (time, params, source) {
    source.stop(time);
  },

  _source: function (params) {},

  _getLocalDestination: function () {

    if (!destination) {
      destination = gainPool();
      destination.connect(this.context.destination);
    }
    destination.gain.value = (this.vent.bap.volume / 100) * 0.8;
    return destination;
  },

  _connectDestination: function (connections, params) {
    var localDestination = this._getLocalDestination();
    if (params.bypass === true) return localDestination;

    // connecting the graph backwards, to start with hooking up to destination
    // and finishing with whatever node will be returned as layer destination
    connections.reverse().forEach(function (connection, index, list) {
      var next = list[index - 1];
      var node = connection.getNode(next && next.cid || 'destination');
      if (node) {
        if (node.__connectedTo && node.__connectedTo !== localDestination) {
          node.disconnect();
        }

        // connecting and disconnecting is causing clicks, even in chrome

        // maybe having two permanent audio flows for each effect:
        //   - active + bypass
        //     - active goes to dry/wet > fx node > output
        //     - bypass goes directly to output

        // that still doesn't solve firefox performance problem, and will likely make it worse

        if (params.bypass === connection.type || Array.isArray(params.bypass) && ~params.bypass.indexOf(connection.type)) {
          return node.disconnect();;
        }

        node.connect(localDestination);
        node.__connectedTo = localDestination;
        localDestination = node;
      }
    });
    return localDestination;
  },

  _configurePan: function (params, out) {
    if (params.pan !== 0) {
      var panner = pannerPool();
      var x = params.pan / 100;
      var z = 1 - Math.abs(x);
      panner.setPosition(x, 0, z);
      panner.connect(out);
      return panner;
    }
  },

  _configureAttack: function (time, params, gain, destination) {
    var volume = (params.volume || 100) / 100;
    gain.connect(destination);
    gain.gain.setValueAtTime(0, time);
    gain.gain.linearRampToValueAtTime(volume, time + params.attack);
  },

  _configureRelease: function (time, params, gain) {
    var volume = (params.volume || 100) / 100;
    gain.gain.setValueAtTime(volume, time);
    gain.gain.linearRampToValueAtTime(0, time + params.release);
  }
}, connectable);

module.exports = Layer;
<commit_msg>smooth transition to pre-release volume<commit_after>var Model = require('./Model');
var triggerParams = require('./mixins/triggerParams');
var volumeParams = require('./mixins/volumeParams');
var connectable = require('./mixins/connectable');
var bypassable = require('./mixins/bypassable');
var Params = require('./Params');

var context = null;

function createPool (factoryName) {
  var nodes = [];
  return function pool(node) {
    if (node) {
      nodes.push(node);
    }
    else if (nodes.length) {
      return nodes.shift();
    }
    else {
      return context[factoryName]();
    }
  };
}

var gainPool = createPool('createGain');
var pannerPool = createPool('createPanner');

var destination = null;

var Layer = Model.extend(triggerParams, volumeParams, {

  type: 'layer',

  props: {
    sources: ['object', true, function () { return {}; }]
  },

  start: function (time, note, channel, pattern) {
    if (this.mute) { return; }

    if (typeof time !== 'number') {
      pattern = channel;
      channel = note;
      note = time;
      time = this.context.currentTime;
    }
    note = note || {};

    context = context || this.context;

    var params = this._params(note, channel, pattern);
    var source = this._source(params);

    if (source) {
      var connections = this._getConnections(note, channel, pattern);
      var destination = this._connectDestination(connections, params);
      var gain = source._gain = gainPool();
      this._configureAttack(time, params, gain, destination);
      var panner = this._configurePan(params, gain);
      source.connect(panner || gain);

      this._startSource(time, params, source);

      var cid = note.cid || 'null';
      this.sources[cid] = this.sources[cid] || [];
      this.sources[cid].push(source);

      if (params.length) {
        var stopTime = time + params.length;
        this.stop(stopTime, note, channel, pattern);
      }

      source.onended = function () {
        var index = this.sources[cid].indexOf(source);
        this.sources[cid].splice(index, 1);

        gain.disconnect();
        source.disconnect();

        if (panner) {
          panner.disconnect();
          pannerPool(panner);
        }

        gainPool(gain);

        source = source.onended = source._gain = gain = panner = null;
        if (typeof note.after === 'function') {
          note.after();
        }
      }.bind(this);
    }
  },

  stop: function (time, note, channel, pattern) {
    if (typeof time !== 'number') {
      pattern = channel;
      channel = note;
      note = time;
      time = this.context.currentTime;
    }
    note = note || {};

    var params = this._params(note, channel, pattern);
    var cid = note.cid || 'null';
    var sources = this.sources[cid] || [];

    if (cid === 'null') {
      sources = [];
      Object.keys(this.sources).forEach(function (cid) {
        sources = sources.concat(this.sources[cid]);
      }.bind(this));
    }

    sources.forEach(function (source) {
      if (source._gain) {
        this._configureRelease(time - params.release, params, source._gain);
      }
      this._stopSource(time, params, source);
    }.bind(this));
  },

  _paramsSources: function (note, channel, pattern) {
    var slot = this.collection && this.collection.parent || {};
    var kit = slot && slot.kit || {};
    return [note, channel, { volume: pattern.volume }, this, slot, kit];
  },

  _params: function (note, channel, pattern) {
    var sources = this._paramsSources(note, channel, pattern);
    return Params.fromSources(sources);
  },

  _getConnections: function (note, channel, pattern) {
    var sources = this._paramsSources(note, channel, pattern);
    var connections = [];
    sources.forEach(function (source) {
      if (source && source.connections) {
        connections = connections.concat(source.connections);
      }
    });
    return connections;
  },

  _createGain: function (params, source) {
    return this.context.createGain();
  },

  _startSource: function (time, params, source) {
    source.start(time);
  },

  _stopSource: function (time, params, source) {
    source.stop(time);
  },

  _source: function (params) {},

  _getLocalDestination: function () {

    if (!destination) {
      destination = gainPool();
      destination.connect(this.context.destination);
    }
    destination.gain.value = (this.vent.bap.volume / 100) * 0.8;
    return destination;
  },

  _connectDestination: function (connections, params) {
    var localDestination = this._getLocalDestination();
    if (params.bypass === true) return localDestination;

    // connecting the graph backwards, to start with hooking up to destination
    // and finishing with whatever node will be returned as layer destination
    connections.reverse().forEach(function (connection, index, list) {
      var next = list[index - 1];
      var node = connection.getNode(next && next.cid || 'destination');
      if (node) {
        if (node.__connectedTo && node.__connectedTo !== localDestination) {
          node.disconnect();
        }

        // connecting and disconnecting is causing clicks, even in chrome

        // maybe having two permanent audio flows for each effect:
        //   - active + bypass
        //     - active goes to dry/wet > fx node > output
        //     - bypass goes directly to output

        // that still doesn't solve firefox performance problem, and will likely make it worse

        if (params.bypass === connection.type || Array.isArray(params.bypass) && ~params.bypass.indexOf(connection.type)) {
          return node.disconnect();;
        }

        node.connect(localDestination);
        node.__connectedTo = localDestination;
        localDestination = node;
      }
    });
    return localDestination;
  },

  _configurePan: function (params, out) {
    if (params.pan !== 0) {
      var panner = pannerPool();
      var x = params.pan / 100;
      var z = 1 - Math.abs(x);
      panner.setPosition(x, 0, z);
      panner.connect(out);
      return panner;
    }
  },

  _configureAttack: function (time, params, gain, destination) {
    var volume = (params.volume || 100) / 100;
    gain.connect(destination);
    gain.gain.setValueAtTime(0, time);
    gain.gain.linearRampToValueAtTime(volume, time + params.attack);
  },

  _configureRelease: function (time, params, gain) {
    var volume = (params.volume || 100) / 100;
    gain.gain.cancelScheduledValues(time);
    gain.gain.linearRampToValueAtTime(volume, time);
    gain.gain.linearRampToValueAtTime(0, time + params.release);
  }
}, connectable);

module.exports = Layer;
<|endoftext|>


<commit_before>// -*- indent-tabs-mode: nil; js2-basic-offset: 2 -*-

var utils = require('./test-utils');
var assert = require('chai').assert;

var BooleanQueryTranslator = require('../lib/bq-translator').BooleanQueryTranslator;
var Domain = require('../lib/database').Domain;

function createTranslator(query) {
  var translator = new BooleanQueryTranslator(query);
  translator.domain = domain;
  translator.defaultFieldNames = ["field"];
  return translator;
}

function testQuery(label, query, expected, customSetup) {
  test('query: ' + label + ': ' +
       '<' + query + '> -> <' + expected + '>', function() {
    if (customSetup) customSetup();
    var translator = createTranslator(query);
    assert.equal(translator.translate(),
                 expected);
  });
}

function testQueryError(label, query, context, detail) {
  test('error: query: ' + label + ': ' + '<' + query + '>', function() {
    var translator = createTranslator(query);
    var actualError;
    assert.throw(function() {
      try {
        translator.translate();
      } catch (error) {
        actualError = error;
        throw error;
      }
    });
    assert.equal(actualError.message, "<" + context + ">" + ": " + detail);
  });
}

function testGroup(label, group, expectedOffset, expectedScriptGrnExpr) {
  test('gorup: ' + label + ': ' +
       '<' + group + '> -> <' + expectedScriptGrnExpr + '>', function() {
    var translator = createTranslator(group);
    var actualScriptGrnExpr = translator.translateGroup();
    assert.deepEqual({
                       scriptGrnExpr: actualScriptGrnExpr,
                       offset: translator.offset
                     },
                     {
                       scriptGrnExpr: expectedScriptGrnExpr,
                       offset: expectedOffset
                     });
  });
}

function testGroupError(label, group, context, detail) {
  test('error: group: ' + label + ': ' + '<' + group + '>', function() {
    var translator = createTranslator(group);
    var actualError;
    assert.throw(function() {
      try {
        translator.translateGroup();
      } catch (error) {
        actualError = error;
        throw error;
      }
    });
    assert.equal(actualError.message, "<" + context + ">" + ": " + detail);
  });
}

function testExpression(label, expression,
                        expectedOffset, expectedScriptGrnExpr) {
  test('expression: ' + label + ': ' +
       '<' + expression + '> -> <' + expectedScriptGrnExpr + '>', function() {
    var translator = createTranslator(expression);
    var actualScriptGrnExpr =
          translator.translateExpression();
    assert.deepEqual({
                       scriptGrnExpr: actualScriptGrnExpr,
                       offset: translator.offset
                     },
                     {
                       scriptGrnExpr: expectedScriptGrnExpr,
                       offset: expectedOffset
                     });
  });
}

function testExpressionError(label, expression, context, detail) {
  test('error: expression: ' + label + ': ' + '<' + expression + '>',
       function() {
    var translator = createTranslator(expression);
    var actualError;
    assert.throw(function() {
      try {
        translator.translateExpression();
      } catch (error) {
        actualError = error;
        throw error;
      }
    });
    assert.equal(actualError.message, "<" + context + ">" + ": " + detail);
  });
}

function testDefaultFieldNames(label, query, defaultFieldNames, expected) {
  test('default field names: ' + label + ': ' +
       '<' + query + '> -> <' + expected + '>', function() {
    var translator = createTranslator(query);
    translator.defaultFieldNames = defaultFieldNames;
    assert.equal(translator.translate(),
                 expected);
  });
}

function testDefaultFieldNamesError(label, query, defaultFieldNames,
                                    context, detail) {
  test('error: default field names: ' + label + ': ' + '<' + query + '>',
       function() {
    var translator = createTranslator(query);
    translator.defaultFieldNames = defaultFieldNames;
    var actualError;
    assert.throw(function() {
      try {
        translator.translate();
      } catch (error) {
        actualError = error;
        throw error;
      }
    });
    assert.equal(actualError.message, "<" + context + ">" + ": " + detail);
  });
}

function testSynonym(label, query, synonyms, expected) {
  test('default synonym: ' + label + ': ' +
       '<' + query + '> -> <' + expected + '>', function() {
    var translator = createTranslator(query);
    translator.domain = {
      getSynonymSync: function(key) {
        return synonyms[key];
      }
    };
    assert.equal(translator.translate(), expected);
  });
}

var context;
var temporaryDatabase;
var domain;

suite('BoolanQueryTranslator', function() {
  setup(function() {
    temporaryDatabase = utils.createTemporaryDatabase();
    context = temporaryDatabase.get();
    domain = new Domain('test', context).createSync();
  });

  teardown(function() {
    context = undefined;
    temporaryDatabase.clear();
    temporaryDatabase.teardown();
    temporaryDatabase = undefined;
  });

  testQuery("expression",
            "type:'ModelName'",
            'type @ "ModelName"');
  testQuery("multiple words expression",
            "type:'Model Name'",
            'type @ "Model" && type @ "Name"');
  testQuery("multiple words expression for literal column",
            "literalfield:'Model Name'",
            'literalfield == "Model Name"',
            function() {
              domain.getIndexField('literalfield').setType('literal')
               .setFacetEnabled(true).setSearchEnabled(true).createSync();
            });
  testQuery("group: raw expressions",
            "(and field1:'keyword1' field2:'keyword2' type:'ModelName')",
            '(field1 @ "keyword1" && field2 @ "keyword2" && type @ "ModelName")');
  testQuery("group: quoted expression",
            "(and 'keyword1 keyword2' type:'ModelName')",
            '(field @ "keyword1" && field @ "keyword2" && type @ "ModelName")');

  testQuery("label",
            "(label 'keyword1 keyword2')",
            'field @ "keyword1" && field @ "keyword2"');

  testQueryError("garbage",
                 "(and 'keyword' type:'ModelName') garbage1 garbage2",
                 "(and 'keyword' type:'ModelName')| |garbage1 garbage2",
                 "garbages exist after valid boolean query");

  testQueryError("label: missing close parentheis",
                 "(label 'keyword1 keyword2'",
                 "(label 'keyword1 keyword2'||",
                 "close parenthesis is missing: operator:<label>");

  testGroup("field",
            "(field field1 'keyword1') (other group)",
            "(field field1 'keyword1')".length,
            "field1 @ \"keyword1\"");
  testGroup("filter",
            "(filter field1 29) (other group)",
            "(filter field1 29)".length,
            "field1 == 29");
  testGroup("and",
            "(and field1:'keyword1' field2:'keyword2') (other group)",
            "(and field1:'keyword1' field2:'keyword2')".length,
            "(field1 @ \"keyword1\" && field2 @ \"keyword2\")");
  testGroup("or",
            "(or field1:'keyword1' field2:'keyword2') (other group)",
            "(or field1:'keyword1' field2:'keyword2')".length,
            "(field1 @ \"keyword1\" || field2 @ \"keyword2\")");
  testGroup("nested",
            "(and (or field1:'k1' field2:'k2') field3:'k3') (other group)",
            "(and (or field1:'k1' field2:'k2') field3:'k3')".length,
            "((field1 @ \"k1\" || field2 @ \"k2\") && field3 @ \"k3\")");

  testGroupError("missing open parentheis",
                 "and field1:'k1' field2:'k2')",
                 "|a|nd field1:'k1' field2:'k2')",
                 "open parenthesis is missing");
  testGroupError("unknown operator",
                 "(nonexistent field1:'k1' field2:'k2')",
                 "(nonexistent| |field1:'k1' field2:'k2')",
                 "unknown operator: <nonexistent>");
  testGroupError("missing close parentheis: in operator",
                 "(an",
                 "(an||",
                 "close parenthesis is missing");
  testGroupError("missing operator",
                 "()",
                 "(|)|",
                 "operator is missing");
  testGroupError("invalid operator character",
                 "(operat0r field1:'k1' field2:'k2')",
                 "(operat|0|r field1:'k1' field2:'k2')",
                 "invalid operator character: <0>");

  testGroupError("field: garbage after value",
                 "(field field1 'k1' 'garbage')",
                 "(field field1 'k1' |'|garbage')",
                 "a garbage character after value: <'>");
  testGroupError("field: no field",
                 "(field )",
                 "(field |)|",
                 "field is missing");
  testGroupError("field: no value",
                 "(field field1)",
                 "(field field1|)|",
                 "field value is missing: field:<field1>");
  testGroupError("field: not string value",
                 "(field field1 29)",
                 "(field field1 |2|9)",
                 "open single quote for string value is missing");
  testGroupError("field: invalid field name",
                 "(field fIeld 'value')",
                 "(field f|I|eld 'value')",
                 "invalid field character: <I>");
  testGroupError("field: missing close parenthesis",
                 "(field ",
                 "(field ||",
                 "close parenthesis is missing: operator:<field>");

  testGroupError("filter: garbage after value",
                 "(filter field1 29 'garbage')",
                 "(filter field1 29 |'|garbage')",
                 "a garbage character after value: <'>");
  testGroupError("filter: no field",
                 "(filter )",
                 "(filter |)|",
                 "field is missing");
  testGroupError("filter: no value",
                 "(filter field1)",
                 "(filter field1|)|",
                 "field value is missing: field:<field1>");
  testGroupError("filter: invalid field name",
                 "(filter fIeld 'value')",
                 "(filter f|I|eld 'value')",
                 "invalid field character: <I>");
  testGroupError("filter: missing close parenthesis",
                 "(filter ",
                 "(filter ||",
                 "close parenthesis is missing: operator:<filter>");

  testGroupError("and: missing close parentheis",
                 "(and field1:'k1' field2:'k2'",
                 "(and field1:'k1' field2:'k2'||",
                 "close parenthesis is missing: operator:<and>");

  testExpression("value only: stirng: and: space",
                 "'keyword1 keyword2' 'other keyword'",
                 "'keyword1 keyword2'".length,
                 "field @ \"keyword1\" && field @ \"keyword2\"");
  testExpression("value only: stirng: and: +",
                 "'keyword1+keyword2' 'other keyword'",
                 "'keyword1+keyword2'".length,
                 "field @ \"keyword1\" && field @ \"keyword2\"");
  testExpression("value only: stirng: or",
                 "'keyword1|keyword2' 'other keyword'",
                 "'keyword1|keyword2'".length,
                 "field @ \"keyword1\" || field @ \"keyword2\"");
  testExpression("value only: stirng: prefix search",
                 "'keyword*' 'other keyword'",
                 "'keyword*'".length,
                 "field @^ \"keyword\"");
  testExpression("value only: stirng: phrase: one",
                 "'\"keyword1 keyword2\"' 'other keyword'",
                 "'\"keyword1 keyword2\"'".length,
                 "field @ \"keyword1 keyword2\"");
  testExpression("value only: stirng: phrase: multi",
                 "'\"keyword1 keyword2\"|\"keyword3\"' 'other keyword'",
                 "'\"keyword1 keyword2\"|\"keyword3\"'".length,
                 "field @ \"keyword1 keyword2\" || field @ \"keyword3\"");

  testExpression("field value: string",
                 "field1:'keyword1 keyword2' field2:'other keyword'",
                 "field1:'keyword1 keyword2'".length,
                 "field1 @ \"keyword1\" && field1 @ \"keyword2\"");
  testExpression("field value: unsigned integer",
                 "field1:29 field2:75",
                 "field1:29".length,
                 "field1 == 29");

  testExpressionError("missing field value separator: normal field name",
                      "field1 'k1'",
                      "field1| |'k1'",
                      "field value separator is missing");
  testExpressionError("missing field value separator: " +
                        "unsigned integer like field name",
                      "29 75",
                      "29| |75",
                      "field value separator is missing");
  testExpressionError("invalid value",
                      "field1:value",
                      "field1:|v|alue",
                      "invalid value: field:<field1>");

  testExpressionError("value only: string: missing close quote",
                      "'k1",
                      "'k1||",
                      "close single quote for string value is missing");
  testExpressionError("value only: stirng: " +
                      "missing operator between keyword and phrase",
                      "'keyword1\"keyword2\"' 'other keyword'",
                      "'keyword1|\"|keyword2\"' 'other keyword'",
                      "operator is missing: keyword:<keyword1>");
  testExpressionError("value only: stirng: missing escaped value",
                      "'keyword1\\",
                      "'keyword1\\||",
                      "escaped character is missing in string value");

  testExpressionError("value only: phrase: missing escaped value",
                      "'\"keyword1\\",
                      "'\"keyword1\\||",
                      "escaped character is missing in phrase");
  testExpressionError("value only: phrase: missing close quote",
                      "'\"keyword",
                      "'\"keyword||",
                      "close double quote for phrase is missing");

  testExpressionError("value only: unsigned integer range: no min and max",
                      "..",
                      "..||",
                      "both min and max are missing");

  testDefaultFieldNames("multi",
                        "'ModelName'",
                        ["type", "name"],
                        '(type @ "ModelName" || name @ "ModelName")');
  testDefaultFieldNamesError("null",
                             "'ModelName'",
                             null,
                             "'ModelName'||",
                             "default fields are missing");
  testDefaultFieldNamesError("empty",
                             "'ModelName'",
                             [],
                             "'ModelName'||",
                             "no default field");

  testSynonym("keyword: existent: 0 synonym",
              "'tokio'",
              { tokio: [] },
              '');
  testSynonym("keyword: existent: 1 synonym",
              "'tokio'",
              { tokio: ["tokyo"] },
              'field @ "tokyo"');
  testSynonym("keyword: existent: N synonyms",
              "'tokio'",
              { tokio: ["tokio", "tokyo"] },
              '(field @ "tokio" || field @ "tokyo")');
  testSynonym("keyword: nonexistent",
              "'hokkaido'",
              { tokio: ["tokio", "tokyo"] },
              'field @ "hokkaido"');

  testSynonym("phrase: existent: 0 synonym",
              "'\"tokyo disney land\"'",
              { "tokyo disney land": [] },
              '');
  testSynonym("phrase: existent: 1 synonym",
              "'\"tokyo disney land\"'",
              { "tokyo disney land": ["TDL"] },
              'field @ "TDL"');
  testSynonym("phrase: existent: N synonyms",
              "'\"tokyo disney land\"'",
              { "tokyo disney land": ["tokyo disney land", "TDL"] },
              '(field @ "tokyo disney land" || field @ "TDL")');
  testSynonym("phrase: nonexistent",
              "'\"tokyo disney land\"'",
              { tokio: ["tokio", "tokyo"] },
              'field @ "tokyo disney land"');
});
<commit_msg>Add a test for bq expression about literal field<commit_after>// -*- indent-tabs-mode: nil; js2-basic-offset: 2 -*-

var utils = require('./test-utils');
var assert = require('chai').assert;

var BooleanQueryTranslator = require('../lib/bq-translator').BooleanQueryTranslator;
var Domain = require('../lib/database').Domain;

function createTranslator(query) {
  var translator = new BooleanQueryTranslator(query);
  translator.domain = domain;
  translator.defaultFieldNames = ["field"];
  return translator;
}

function testQuery(label, query, expected, customSetup) {
  test('query: ' + label + ': ' +
       '<' + query + '> -> <' + expected + '>', function() {
    if (customSetup) customSetup();
    var translator = createTranslator(query);
    assert.equal(translator.translate(),
                 expected);
  });
}

function testQueryError(label, query, context, detail) {
  test('error: query: ' + label + ': ' + '<' + query + '>', function() {
    var translator = createTranslator(query);
    var actualError;
    assert.throw(function() {
      try {
        translator.translate();
      } catch (error) {
        actualError = error;
        throw error;
      }
    });
    assert.equal(actualError.message, "<" + context + ">" + ": " + detail);
  });
}

function testGroup(label, group, expectedOffset, expectedScriptGrnExpr) {
  test('gorup: ' + label + ': ' +
       '<' + group + '> -> <' + expectedScriptGrnExpr + '>', function() {
    var translator = createTranslator(group);
    var actualScriptGrnExpr = translator.translateGroup();
    assert.deepEqual({
                       scriptGrnExpr: actualScriptGrnExpr,
                       offset: translator.offset
                     },
                     {
                       scriptGrnExpr: expectedScriptGrnExpr,
                       offset: expectedOffset
                     });
  });
}

function testGroupError(label, group, context, detail) {
  test('error: group: ' + label + ': ' + '<' + group + '>', function() {
    var translator = createTranslator(group);
    var actualError;
    assert.throw(function() {
      try {
        translator.translateGroup();
      } catch (error) {
        actualError = error;
        throw error;
      }
    });
    assert.equal(actualError.message, "<" + context + ">" + ": " + detail);
  });
}

function testExpression(label, expression,
                        expectedOffset, expectedScriptGrnExpr) {
  test('expression: ' + label + ': ' +
       '<' + expression + '> -> <' + expectedScriptGrnExpr + '>', function() {
    var translator = createTranslator(expression);
    var actualScriptGrnExpr =
          translator.translateExpression();
    assert.deepEqual({
                       scriptGrnExpr: actualScriptGrnExpr,
                       offset: translator.offset
                     },
                     {
                       scriptGrnExpr: expectedScriptGrnExpr,
                       offset: expectedOffset
                     });
  });
}

function testExpressionError(label, expression, context, detail) {
  test('error: expression: ' + label + ': ' + '<' + expression + '>',
       function() {
    var translator = createTranslator(expression);
    var actualError;
    assert.throw(function() {
      try {
        translator.translateExpression();
      } catch (error) {
        actualError = error;
        throw error;
      }
    });
    assert.equal(actualError.message, "<" + context + ">" + ": " + detail);
  });
}

function testDefaultFieldNames(label, query, defaultFieldNames, expected) {
  test('default field names: ' + label + ': ' +
       '<' + query + '> -> <' + expected + '>', function() {
    var translator = createTranslator(query);
    translator.defaultFieldNames = defaultFieldNames;
    assert.equal(translator.translate(),
                 expected);
  });
}

function testDefaultFieldNamesError(label, query, defaultFieldNames,
                                    context, detail) {
  test('error: default field names: ' + label + ': ' + '<' + query + '>',
       function() {
    var translator = createTranslator(query);
    translator.defaultFieldNames = defaultFieldNames;
    var actualError;
    assert.throw(function() {
      try {
        translator.translate();
      } catch (error) {
        actualError = error;
        throw error;
      }
    });
    assert.equal(actualError.message, "<" + context + ">" + ": " + detail);
  });
}

function testSynonym(label, query, synonyms, expected) {
  test('default synonym: ' + label + ': ' +
       '<' + query + '> -> <' + expected + '>', function() {
    var translator = createTranslator(query);
    translator.domain = {
      getSynonymSync: function(key) {
        return synonyms[key];
      }
    };
    assert.equal(translator.translate(), expected);
  });
}

var context;
var temporaryDatabase;
var domain;

suite('BoolanQueryTranslator', function() {
  setup(function() {
    temporaryDatabase = utils.createTemporaryDatabase();
    context = temporaryDatabase.get();
    domain = new Domain('test', context).createSync();
    domain.getIndexField('literalfield').setType('literal')
      .setFacetEnabled(true).setSearchEnabled(true).createSync();
  });

  teardown(function() {
    context = undefined;
    temporaryDatabase.clear();
    temporaryDatabase.teardown();
    temporaryDatabase = undefined;
  });

  testQuery("expression",
            "type:'ModelName'",
            'type @ "ModelName"');
  testQuery("expression for literal column",
            "literalfield:'ModelName'",
            'literalfield == "ModelName"');
  testQuery("multiple words expression",
            "type:'Model Name'",
            'type @ "Model" && type @ "Name"');
  testQuery("multiple words expression for literal column",
            "literalfield:'Model Name'",
            'literalfield == "Model Name"');
  testQuery("group: raw expressions",
            "(and field1:'keyword1' field2:'keyword2' type:'ModelName')",
            '(field1 @ "keyword1" && field2 @ "keyword2" && type @ "ModelName")');
  testQuery("group: quoted expression",
            "(and 'keyword1 keyword2' type:'ModelName')",
            '(field @ "keyword1" && field @ "keyword2" && type @ "ModelName")');

  testQuery("label",
            "(label 'keyword1 keyword2')",
            'field @ "keyword1" && field @ "keyword2"');

  testQueryError("garbage",
                 "(and 'keyword' type:'ModelName') garbage1 garbage2",
                 "(and 'keyword' type:'ModelName')| |garbage1 garbage2",
                 "garbages exist after valid boolean query");

  testQueryError("label: missing close parentheis",
                 "(label 'keyword1 keyword2'",
                 "(label 'keyword1 keyword2'||",
                 "close parenthesis is missing: operator:<label>");

  testGroup("field",
            "(field field1 'keyword1') (other group)",
            "(field field1 'keyword1')".length,
            "field1 @ \"keyword1\"");
  testGroup("filter",
            "(filter field1 29) (other group)",
            "(filter field1 29)".length,
            "field1 == 29");
  testGroup("and",
            "(and field1:'keyword1' field2:'keyword2') (other group)",
            "(and field1:'keyword1' field2:'keyword2')".length,
            "(field1 @ \"keyword1\" && field2 @ \"keyword2\")");
  testGroup("or",
            "(or field1:'keyword1' field2:'keyword2') (other group)",
            "(or field1:'keyword1' field2:'keyword2')".length,
            "(field1 @ \"keyword1\" || field2 @ \"keyword2\")");
  testGroup("nested",
            "(and (or field1:'k1' field2:'k2') field3:'k3') (other group)",
            "(and (or field1:'k1' field2:'k2') field3:'k3')".length,
            "((field1 @ \"k1\" || field2 @ \"k2\") && field3 @ \"k3\")");

  testGroupError("missing open parentheis",
                 "and field1:'k1' field2:'k2')",
                 "|a|nd field1:'k1' field2:'k2')",
                 "open parenthesis is missing");
  testGroupError("unknown operator",
                 "(nonexistent field1:'k1' field2:'k2')",
                 "(nonexistent| |field1:'k1' field2:'k2')",
                 "unknown operator: <nonexistent>");
  testGroupError("missing close parentheis: in operator",
                 "(an",
                 "(an||",
                 "close parenthesis is missing");
  testGroupError("missing operator",
                 "()",
                 "(|)|",
                 "operator is missing");
  testGroupError("invalid operator character",
                 "(operat0r field1:'k1' field2:'k2')",
                 "(operat|0|r field1:'k1' field2:'k2')",
                 "invalid operator character: <0>");

  testGroupError("field: garbage after value",
                 "(field field1 'k1' 'garbage')",
                 "(field field1 'k1' |'|garbage')",
                 "a garbage character after value: <'>");
  testGroupError("field: no field",
                 "(field )",
                 "(field |)|",
                 "field is missing");
  testGroupError("field: no value",
                 "(field field1)",
                 "(field field1|)|",
                 "field value is missing: field:<field1>");
  testGroupError("field: not string value",
                 "(field field1 29)",
                 "(field field1 |2|9)",
                 "open single quote for string value is missing");
  testGroupError("field: invalid field name",
                 "(field fIeld 'value')",
                 "(field f|I|eld 'value')",
                 "invalid field character: <I>");
  testGroupError("field: missing close parenthesis",
                 "(field ",
                 "(field ||",
                 "close parenthesis is missing: operator:<field>");

  testGroupError("filter: garbage after value",
                 "(filter field1 29 'garbage')",
                 "(filter field1 29 |'|garbage')",
                 "a garbage character after value: <'>");
  testGroupError("filter: no field",
                 "(filter )",
                 "(filter |)|",
                 "field is missing");
  testGroupError("filter: no value",
                 "(filter field1)",
                 "(filter field1|)|",
                 "field value is missing: field:<field1>");
  testGroupError("filter: invalid field name",
                 "(filter fIeld 'value')",
                 "(filter f|I|eld 'value')",
                 "invalid field character: <I>");
  testGroupError("filter: missing close parenthesis",
                 "(filter ",
                 "(filter ||",
                 "close parenthesis is missing: operator:<filter>");

  testGroupError("and: missing close parentheis",
                 "(and field1:'k1' field2:'k2'",
                 "(and field1:'k1' field2:'k2'||",
                 "close parenthesis is missing: operator:<and>");

  testExpression("value only: stirng: and: space",
                 "'keyword1 keyword2' 'other keyword'",
                 "'keyword1 keyword2'".length,
                 "field @ \"keyword1\" && field @ \"keyword2\"");
  testExpression("value only: stirng: and: +",
                 "'keyword1+keyword2' 'other keyword'",
                 "'keyword1+keyword2'".length,
                 "field @ \"keyword1\" && field @ \"keyword2\"");
  testExpression("value only: stirng: or",
                 "'keyword1|keyword2' 'other keyword'",
                 "'keyword1|keyword2'".length,
                 "field @ \"keyword1\" || field @ \"keyword2\"");
  testExpression("value only: stirng: prefix search",
                 "'keyword*' 'other keyword'",
                 "'keyword*'".length,
                 "field @^ \"keyword\"");
  testExpression("value only: stirng: phrase: one",
                 "'\"keyword1 keyword2\"' 'other keyword'",
                 "'\"keyword1 keyword2\"'".length,
                 "field @ \"keyword1 keyword2\"");
  testExpression("value only: stirng: phrase: multi",
                 "'\"keyword1 keyword2\"|\"keyword3\"' 'other keyword'",
                 "'\"keyword1 keyword2\"|\"keyword3\"'".length,
                 "field @ \"keyword1 keyword2\" || field @ \"keyword3\"");

  testExpression("field value: string",
                 "field1:'keyword1 keyword2' field2:'other keyword'",
                 "field1:'keyword1 keyword2'".length,
                 "field1 @ \"keyword1\" && field1 @ \"keyword2\"");
  testExpression("field value: unsigned integer",
                 "field1:29 field2:75",
                 "field1:29".length,
                 "field1 == 29");

  testExpressionError("missing field value separator: normal field name",
                      "field1 'k1'",
                      "field1| |'k1'",
                      "field value separator is missing");
  testExpressionError("missing field value separator: " +
                        "unsigned integer like field name",
                      "29 75",
                      "29| |75",
                      "field value separator is missing");
  testExpressionError("invalid value",
                      "field1:value",
                      "field1:|v|alue",
                      "invalid value: field:<field1>");

  testExpressionError("value only: string: missing close quote",
                      "'k1",
                      "'k1||",
                      "close single quote for string value is missing");
  testExpressionError("value only: stirng: " +
                      "missing operator between keyword and phrase",
                      "'keyword1\"keyword2\"' 'other keyword'",
                      "'keyword1|\"|keyword2\"' 'other keyword'",
                      "operator is missing: keyword:<keyword1>");
  testExpressionError("value only: stirng: missing escaped value",
                      "'keyword1\\",
                      "'keyword1\\||",
                      "escaped character is missing in string value");

  testExpressionError("value only: phrase: missing escaped value",
                      "'\"keyword1\\",
                      "'\"keyword1\\||",
                      "escaped character is missing in phrase");
  testExpressionError("value only: phrase: missing close quote",
                      "'\"keyword",
                      "'\"keyword||",
                      "close double quote for phrase is missing");

  testExpressionError("value only: unsigned integer range: no min and max",
                      "..",
                      "..||",
                      "both min and max are missing");

  testDefaultFieldNames("multi",
                        "'ModelName'",
                        ["type", "name"],
                        '(type @ "ModelName" || name @ "ModelName")');
  testDefaultFieldNamesError("null",
                             "'ModelName'",
                             null,
                             "'ModelName'||",
                             "default fields are missing");
  testDefaultFieldNamesError("empty",
                             "'ModelName'",
                             [],
                             "'ModelName'||",
                             "no default field");

  testSynonym("keyword: existent: 0 synonym",
              "'tokio'",
              { tokio: [] },
              '');
  testSynonym("keyword: existent: 1 synonym",
              "'tokio'",
              { tokio: ["tokyo"] },
              'field @ "tokyo"');
  testSynonym("keyword: existent: N synonyms",
              "'tokio'",
              { tokio: ["tokio", "tokyo"] },
              '(field @ "tokio" || field @ "tokyo")');
  testSynonym("keyword: nonexistent",
              "'hokkaido'",
              { tokio: ["tokio", "tokyo"] },
              'field @ "hokkaido"');

  testSynonym("phrase: existent: 0 synonym",
              "'\"tokyo disney land\"'",
              { "tokyo disney land": [] },
              '');
  testSynonym("phrase: existent: 1 synonym",
              "'\"tokyo disney land\"'",
              { "tokyo disney land": ["TDL"] },
              'field @ "TDL"');
  testSynonym("phrase: existent: N synonyms",
              "'\"tokyo disney land\"'",
              { "tokyo disney land": ["tokyo disney land", "TDL"] },
              '(field @ "tokyo disney land" || field @ "TDL")');
  testSynonym("phrase: nonexistent",
              "'\"tokyo disney land\"'",
              { tokio: ["tokio", "tokyo"] },
              'field @ "tokyo disney land"');
});
<|endoftext|>