tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

fluent-react.js (19068B)


      1 /* Copyright 2019 Mozilla Foundation and others
      2 *
      3 * Licensed under the Apache License, Version 2.0 (the "License");
      4 * you may not use this file except in compliance with the License.
      5 * You may obtain a copy of the License at
      6 *
      7 *     http://www.apache.org/licenses/LICENSE-2.0
      8 *
      9 * Unless required by applicable law or agreed to in writing, software
     10 * distributed under the License is distributed on an "AS IS" BASIS,
     11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 * See the License for the specific language governing permissions and
     13 * limitations under the License.
     14 */
     15 
     16 
     17 'use strict';
     18 
     19 /* fluent-react@0.10.0 */
     20 
     21 Object.defineProperty(exports, '__esModule', { value: true });
     22 
     23 function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
     24 
     25 const react = require("resource://devtools/client/shared/vendor/react.mjs");
     26 const PropTypes = _interopDefault(require("resource://devtools/client/shared/vendor/react-prop-types.mjs"));
     27 
     28 /*
     29 * Synchronously map an identifier or an array of identifiers to the best
     30 * `FluentBundle` instance(s).
     31 *
     32 * @param {Iterable} iterable
     33 * @param {string|Array<string>} ids
     34 * @returns {FluentBundle|Array<FluentBundle>}
     35 */
     36 function mapBundleSync(iterable, ids) {
     37  if (!Array.isArray(ids)) {
     38    return getBundleForId(iterable, ids);
     39  }
     40 
     41  return ids.map(
     42    id => getBundleForId(iterable, id)
     43  );
     44 }
     45 
     46 /*
     47 * Find the best `FluentBundle` with the translation for `id`.
     48 */
     49 function getBundleForId(iterable, id) {
     50  for (const bundle of iterable) {
     51    if (bundle.hasMessage(id)) {
     52      return bundle;
     53    }
     54  }
     55 
     56  return null;
     57 }
     58 
     59 /*
     60 * Asynchronously map an identifier or an array of identifiers to the best
     61 * `FluentBundle` instance(s).
     62 *
     63 * @param {AsyncIterable} iterable
     64 * @param {string|Array<string>} ids
     65 * @returns {Promise<FluentBundle|Array<FluentBundle>>}
     66 */
     67 
     68 /*
     69 * @module fluent-sequence
     70 * @overview Manage ordered sequences of FluentBundles.
     71 */
     72 
     73 /*
     74 * Base CachedIterable class.
     75 */
     76 class CachedIterable extends Array {
     77    /**
     78     * Create a `CachedIterable` instance from an iterable or, if another
     79     * instance of `CachedIterable` is passed, return it without any
     80     * modifications.
     81     *
     82     * @param {Iterable} iterable
     83     * @returns {CachedIterable}
     84     */
     85    static from(iterable) {
     86        if (iterable instanceof this) {
     87            return iterable;
     88        }
     89 
     90        return new this(iterable);
     91    }
     92 }
     93 
     94 /*
     95 * CachedSyncIterable caches the elements yielded by an iterable.
     96 *
     97 * It can be used to iterate over an iterable many times without depleting the
     98 * iterable.
     99 */
    100 class CachedSyncIterable extends CachedIterable {
    101    /**
    102     * Create an `CachedSyncIterable` instance.
    103     *
    104     * @param {Iterable} iterable
    105     * @returns {CachedSyncIterable}
    106     */
    107    constructor(iterable) {
    108        super();
    109 
    110        if (Symbol.iterator in Object(iterable)) {
    111            this.iterator = iterable[Symbol.iterator]();
    112        } else {
    113            throw new TypeError("Argument must implement the iteration protocol.");
    114        }
    115    }
    116 
    117    [Symbol.iterator]() {
    118        const cached = this;
    119        let cur = 0;
    120 
    121        return {
    122            next() {
    123                if (cached.length <= cur) {
    124                    cached.push(cached.iterator.next());
    125                }
    126                return cached[cur++];
    127            }
    128        };
    129    }
    130 
    131    /**
    132     * This method allows user to consume the next element from the iterator
    133     * into the cache.
    134     *
    135     * @param {number} count - number of elements to consume
    136     */
    137    touchNext(count = 1) {
    138        let idx = 0;
    139        while (idx++ < count) {
    140            const last = this[this.length - 1];
    141            if (last && last.done) {
    142                break;
    143            }
    144            this.push(this.iterator.next());
    145        }
    146        // Return the last cached {value, done} object to allow the calling
    147        // code to decide if it needs to call touchNext again.
    148        return this[this.length - 1];
    149    }
    150 }
    151 
    152 /*
    153 * `ReactLocalization` handles translation formatting and fallback.
    154 *
    155 * The current negotiated fallback chain of languages is stored in the
    156 * `ReactLocalization` instance in form of an iterable of `FluentBundle`
    157 * instances.  This iterable is used to find the best existing translation for
    158 * a given identifier.
    159 *
    160 * `Localized` components must subscribe to the changes of the
    161 * `ReactLocalization`'s fallback chain.  When the fallback chain changes (the
    162 * `bundles` iterable is set anew), all subscribed compontent must relocalize.
    163 *
    164 * The `ReactLocalization` class instances are exposed to `Localized` elements
    165 * via the `LocalizationProvider` component.
    166 */
    167 class ReactLocalization {
    168  constructor(bundles) {
    169    this.bundles = CachedSyncIterable.from(bundles);
    170    this.subs = new Set();
    171  }
    172 
    173  /*
    174   * Subscribe a `Localized` component to changes of `bundles`.
    175   */
    176  subscribe(comp) {
    177    this.subs.add(comp);
    178  }
    179 
    180  /*
    181   * Unsubscribe a `Localized` component from `bundles` changes.
    182   */
    183  unsubscribe(comp) {
    184    this.subs.delete(comp);
    185  }
    186 
    187  /*
    188   * Set a new `bundles` iterable and trigger the retranslation.
    189   */
    190  setBundles(bundles) {
    191    this.bundles = CachedSyncIterable.from(bundles);
    192 
    193    // Update all subscribed Localized components.
    194    this.subs.forEach(comp => comp.relocalize());
    195  }
    196 
    197  getBundle(id) {
    198    return mapBundleSync(this.bundles, id);
    199  }
    200 
    201  /*
    202   * Find a translation by `id` and format it to a string using `args`.
    203   */
    204  getString(id, args, fallback) {
    205    const bundle = this.getBundle(id);
    206    if (bundle) {
    207      const msg = bundle.getMessage(id);
    208      if (msg && msg.value) {
    209        let errors = [];
    210        let value = bundle.formatPattern(msg.value, args, errors);
    211        for (let error of errors) {
    212          this.reportError(error);
    213        }
    214        return value;
    215      }
    216    }
    217 
    218    return fallback || id;
    219  }
    220 
    221  // XXX Control this via a prop passed to the LocalizationProvider.
    222  // See https://github.com/projectfluent/fluent.js/issues/411.
    223  reportError(error) {
    224    /* global console */
    225    // eslint-disable-next-line no-console
    226    console.warn(`[@fluent/react] ${error.name}: ${error.message}`);
    227  }
    228 }
    229 
    230 function isReactLocalization(props, propName) {
    231  const prop = props[propName];
    232 
    233  if (prop instanceof ReactLocalization) {
    234    return null;
    235  }
    236 
    237  return new Error(
    238    `The ${propName} context field must be an instance of ReactLocalization.`
    239  );
    240 }
    241 
    242 /* eslint-env browser */
    243 
    244 let cachedParseMarkup;
    245 
    246 // We use a function creator to make the reference to `document` lazy. At the
    247 // same time, it's eager enough to throw in <LocalizationProvider> as soon as
    248 // it's first mounted which reduces the risk of this error making it to the
    249 // runtime without developers noticing it in development.
    250 function createParseMarkup() {
    251  if (typeof(document) === "undefined") {
    252    // We can't use <template> to sanitize translations.
    253    throw new Error(
    254      "`document` is undefined. Without it, translations cannot " +
    255      "be safely sanitized. Consult the documentation at " +
    256      "https://github.com/projectfluent/fluent.js/wiki/React-Overlays."
    257    );
    258  }
    259 
    260  if (!cachedParseMarkup) {
    261    const template = document.createElement("template");
    262    cachedParseMarkup = function parseMarkup(str) {
    263      template.innerHTML = str;
    264      return Array.from(template.content.childNodes);
    265    };
    266  }
    267 
    268  return cachedParseMarkup;
    269 }
    270 
    271 /*
    272 * The Provider component for the `ReactLocalization` class.
    273 *
    274 * Exposes a `ReactLocalization` instance to all descendants via React's
    275 * context feature.  It makes translations available to all localizable
    276 * elements in the descendant's render tree without the need to pass them
    277 * explicitly.
    278 *
    279 *     <LocalizationProvider bundles={…}>
    280 *         …
    281 *     </LocalizationProvider>
    282 *
    283 * The `LocalizationProvider` component takes one prop: `bundles`.  It should
    284 * be an iterable of `FluentBundle` instances in order of the user's
    285 * preferred languages.  The `FluentBundle` instances will be used by
    286 * `ReactLocalization` to format translations.  If a translation is missing in
    287 * one instance, `ReactLocalization` will fall back to the next one.
    288 */
    289 class LocalizationProvider extends react.Component {
    290  constructor(props) {
    291    super(props);
    292    const {bundles, parseMarkup} = props;
    293 
    294    if (bundles === undefined) {
    295      throw new Error("LocalizationProvider must receive the bundles prop.");
    296    }
    297 
    298    if (!bundles[Symbol.iterator]) {
    299      throw new Error("The bundles prop must be an iterable.");
    300    }
    301 
    302    this.l10n = new ReactLocalization(bundles);
    303    this.parseMarkup = parseMarkup || createParseMarkup();
    304  }
    305 
    306  getChildContext() {
    307    return {
    308      l10n: this.l10n,
    309      parseMarkup: this.parseMarkup,
    310    };
    311  }
    312 
    313  componentWillReceiveProps(next) {
    314    const { bundles } = next;
    315 
    316    if (bundles !== this.props.bundles) {
    317      this.l10n.setBundles(bundles);
    318    }
    319  }
    320 
    321  render() {
    322    return react.Children.only(this.props.children);
    323  }
    324 }
    325 
    326 LocalizationProvider.childContextTypes = {
    327  l10n: isReactLocalization,
    328  parseMarkup: PropTypes.func,
    329 };
    330 
    331 LocalizationProvider.propTypes = {
    332  children: PropTypes.element.isRequired,
    333  bundles: isIterable,
    334  parseMarkup: PropTypes.func,
    335 };
    336 
    337 function isIterable(props, propName, componentName) {
    338  const prop = props[propName];
    339 
    340  if (Symbol.iterator in Object(prop)) {
    341    return null;
    342  }
    343 
    344  return new Error(
    345    `The ${propName} prop supplied to ${componentName} must be an iterable.`
    346  );
    347 }
    348 
    349 function withLocalization(Inner) {
    350  class WithLocalization extends react.Component {
    351    componentDidMount() {
    352      const { l10n } = this.context;
    353 
    354      if (l10n) {
    355        l10n.subscribe(this);
    356      }
    357    }
    358 
    359    componentWillUnmount() {
    360      const { l10n } = this.context;
    361 
    362      if (l10n) {
    363        l10n.unsubscribe(this);
    364      }
    365    }
    366 
    367    /*
    368     * Rerender this component in a new language.
    369     */
    370    relocalize() {
    371      // When the `ReactLocalization`'s fallback chain changes, update the
    372      // component.
    373      this.forceUpdate();
    374    }
    375 
    376    /*
    377     * Find a translation by `id` and format it to a string using `args`.
    378     */
    379    getString(id, args, fallback) {
    380      const { l10n } = this.context;
    381 
    382      if (!l10n) {
    383        return fallback || id;
    384      }
    385 
    386      return l10n.getString(id, args, fallback);
    387    }
    388 
    389    render() {
    390      return react.createElement(
    391        Inner,
    392        Object.assign(
    393          // getString needs to be re-bound on updates to trigger a re-render
    394          { getString: (...args) => this.getString(...args) },
    395          this.props
    396        )
    397      );
    398    }
    399  }
    400 
    401  WithLocalization.displayName = `WithLocalization(${displayName(Inner)})`;
    402 
    403  WithLocalization.contextTypes = {
    404    l10n: isReactLocalization
    405  };
    406 
    407  return WithLocalization;
    408 }
    409 
    410 function displayName(component) {
    411  return component.displayName || component.name || "Component";
    412 }
    413 
    414 /**
    415 * Copyright (c) 2013-present, Facebook, Inc.
    416 *
    417 * This source code is licensed under the MIT license found in the
    418 * LICENSE file in this directory.
    419 */
    420 
    421 // For HTML, certain tags should omit their close tag. We keep a whitelist for
    422 // those special-case tags.
    423 
    424 var omittedCloseTags = {
    425  area: true,
    426  base: true,
    427  br: true,
    428  col: true,
    429  embed: true,
    430  hr: true,
    431  img: true,
    432  input: true,
    433  keygen: true,
    434  link: true,
    435  meta: true,
    436  param: true,
    437  source: true,
    438  track: true,
    439  wbr: true,
    440  // NOTE: menuitem's close tag should be omitted, but that causes problems.
    441 };
    442 
    443 /**
    444 * Copyright (c) 2013-present, Facebook, Inc.
    445 *
    446 * This source code is licensed under the MIT license found in the
    447 * LICENSE file in this directory.
    448 */
    449 
    450 // For HTML, certain tags cannot have children. This has the same purpose as
    451 // `omittedCloseTags` except that `menuitem` should still have its closing tag.
    452 
    453 var voidElementTags = {
    454  menuitem: true,
    455  ...omittedCloseTags,
    456 };
    457 
    458 // Match the opening angle bracket (<) in HTML tags, and HTML entities like
    459 // &amp;, &#0038;, &#x0026;.
    460 const reMarkup = /<|&#?\w+;/;
    461 
    462 /*
    463 * Prepare props passed to `Localized` for formatting.
    464 */
    465 function toArguments(props) {
    466  const args = {};
    467  const elems = {};
    468 
    469  for (const [propname, propval] of Object.entries(props)) {
    470    if (propname.startsWith("$")) {
    471      const name = propname.substr(1);
    472      args[name] = propval;
    473    } else if (react.isValidElement(propval)) {
    474      // We'll try to match localNames of elements found in the translation with
    475      // names of elements passed as props. localNames are always lowercase.
    476      const name = propname.toLowerCase();
    477      elems[name] = propval;
    478    }
    479  }
    480 
    481  return [args, elems];
    482 }
    483 
    484 /*
    485 * The `Localized` class renders its child with translated props and children.
    486 *
    487 *     <Localized id="hello-world">
    488 *         <p>{'Hello, world!'}</p>
    489 *     </Localized>
    490 *
    491 * The `id` prop should be the unique identifier of the translation.  Any
    492 * attributes found in the translation will be applied to the wrapped element.
    493 *
    494 * Arguments to the translation can be passed as `$`-prefixed props on
    495 * `Localized`.
    496 *
    497 *     <Localized id="hello-world" $username={name}>
    498 *         <p>{'Hello, { $username }!'}</p>
    499 *     </Localized>
    500 *
    501 *  It's recommended that the contents of the wrapped component be a string
    502 *  expression.  The string will be used as the ultimate fallback if no
    503 *  translation is available.  It also makes it easy to grep for strings in the
    504 *  source code.
    505 */
    506 class Localized extends react.Component {
    507  componentDidMount() {
    508    const { l10n } = this.context;
    509 
    510    if (l10n) {
    511      l10n.subscribe(this);
    512    }
    513  }
    514 
    515  componentWillUnmount() {
    516    const { l10n } = this.context;
    517 
    518    if (l10n) {
    519      l10n.unsubscribe(this);
    520    }
    521  }
    522 
    523  /*
    524   * Rerender this component in a new language.
    525   */
    526  relocalize() {
    527    // When the `ReactLocalization`'s fallback chain changes, update the
    528    // component.
    529    this.forceUpdate();
    530  }
    531 
    532  render() {
    533    const { l10n, parseMarkup } = this.context;
    534    const { id, attrs, children: child = null } = this.props;
    535 
    536    // Validate that the child element isn't an array
    537    if (Array.isArray(child)) {
    538      throw new Error("<Localized/> expected to receive a single " +
    539        "React node child");
    540    }
    541 
    542    if (!l10n) {
    543      // Use the wrapped component as fallback.
    544      return child;
    545    }
    546 
    547    const bundle = l10n.getBundle(id);
    548 
    549    if (bundle === null) {
    550      // Use the wrapped component as fallback.
    551      return child;
    552    }
    553 
    554    const msg = bundle.getMessage(id);
    555    const [args, elems] = toArguments(this.props);
    556    let errors = [];
    557 
    558    // Check if the child inside <Localized> is a valid element -- if not, then
    559    // it's either null or a simple fallback string. No need to localize the
    560    // attributes.
    561    if (!react.isValidElement(child)) {
    562      if (msg.value) {
    563        // Replace the fallback string with the message value;
    564        let value = bundle.formatPattern(msg.value, args, errors);
    565        for (let error of errors) {
    566          l10n.reportError(error);
    567        }
    568        return value;
    569      }
    570 
    571      return child;
    572    }
    573 
    574    let localizedProps;
    575 
    576    // The default is to forbid all message attributes. If the attrs prop exists
    577    // on the Localized instance, only set message attributes which have been
    578    // explicitly allowed by the developer.
    579    if (attrs && msg.attributes) {
    580      localizedProps = {};
    581      errors = [];
    582      for (const [name, allowed] of Object.entries(attrs)) {
    583        if (allowed && name in msg.attributes) {
    584          localizedProps[name] = bundle.formatPattern(
    585            msg.attributes[name], args, errors);
    586        }
    587      }
    588      for (let error of errors) {
    589        l10n.reportError(error);
    590      }
    591    }
    592 
    593    // If the wrapped component is a known void element, explicitly dismiss the
    594    // message value and do not pass it to cloneElement in order to avoid the
    595    // "void element tags must neither have `children` nor use
    596    // `dangerouslySetInnerHTML`" error.
    597    if (child.type in voidElementTags) {
    598      return react.cloneElement(child, localizedProps);
    599    }
    600 
    601    // If the message has a null value, we're only interested in its attributes.
    602    // Do not pass the null value to cloneElement as it would nuke all children
    603    // of the wrapped component.
    604    if (msg.value === null) {
    605      return react.cloneElement(child, localizedProps);
    606    }
    607 
    608    errors = [];
    609    const messageValue = bundle.formatPattern(msg.value, args, errors);
    610    for (let error of errors) {
    611      l10n.reportError(error);
    612    }
    613 
    614    // If the message value doesn't contain any markup nor any HTML entities,
    615    // insert it as the only child of the wrapped component.
    616    if (!reMarkup.test(messageValue)) {
    617      return react.cloneElement(child, localizedProps, messageValue);
    618    }
    619 
    620    // If the message contains markup, parse it and try to match the children
    621    // found in the translation with the props passed to this Localized.
    622    const translationNodes = parseMarkup(messageValue);
    623    const translatedChildren = translationNodes.map(childNode => {
    624      if (childNode.nodeType === childNode.TEXT_NODE) {
    625        return childNode.textContent;
    626      }
    627 
    628      // If the child is not expected just take its textContent.
    629      if (!elems.hasOwnProperty(childNode.localName)) {
    630        return childNode.textContent;
    631      }
    632 
    633      const sourceChild = elems[childNode.localName];
    634 
    635      // If the element passed as a prop to <Localized> is a known void element,
    636      // explicitly dismiss any textContent which might have accidentally been
    637      // defined in the translation to prevent the "void element tags must not
    638      // have children" error.
    639      if (sourceChild.type in voidElementTags) {
    640        return sourceChild;
    641      }
    642 
    643      // TODO Protect contents of elements wrapped in <Localized>
    644      // https://github.com/projectfluent/fluent.js/issues/184
    645      // TODO  Control localizable attributes on elements passed as props
    646      // https://github.com/projectfluent/fluent.js/issues/185
    647      return react.cloneElement(sourceChild, null, childNode.textContent);
    648    });
    649 
    650    return react.cloneElement(child, localizedProps, ...translatedChildren);
    651  }
    652 }
    653 
    654 Localized.contextTypes = {
    655  l10n: isReactLocalization,
    656  parseMarkup: PropTypes.func,
    657 };
    658 
    659 Localized.propTypes = {
    660  children: PropTypes.node
    661 };
    662 
    663 /*
    664 * @module fluent-react
    665 * @overview
    666 *
    667 
    668 * `fluent-react` provides React bindings for Fluent.  It takes advantage of
    669 * React's Components system and the virtual DOM.  Translations are exposed to
    670 * components via the provider pattern.
    671 *
    672 *     <LocalizationProvider bundles={…}>
    673 *         <Localized id="hello-world">
    674 *             <p>{'Hello, world!'}</p>
    675 *         </Localized>
    676 *     </LocalizationProvider>
    677 *
    678 * Consult the documentation of the `LocalizationProvider` and the `Localized`
    679 * components for more information.
    680 */
    681 
    682 exports.LocalizationProvider = LocalizationProvider;
    683 exports.Localized = Localized;
    684 exports.ReactLocalization = ReactLocalization;
    685 exports.isReactLocalization = isReactLocalization;
    686 exports.withLocalization = withLocalization;