<commit_before>'use strict'

var nest = require('depnest')
var { Value, computed } = require('mutant')
var MutantPullDict = require('../../lib/mutant-pull-dict')

exports.needs = nest({
  'sbot.pull.stream': 'first'
})

exports.gives = nest({
  'contact.obs': ['following', 'followers', 'blocking', 'blockers', 'hops', 'reverseHops']
})

exports.create = function (api) {
  var cache = {}
  var reverseCache = {}

  window.contacts = {
    cache,
    reverseCache
  }

  var sync = Value(false)

  return nest('contact.obs', {
    following: (key) => matchingValueKeys(hops(key), 1),
    followers: (key) => matchingValueKeys(reverseHops(key), 1),
    blocking: (key) => matchingValueKeys(hops(key), -1),
    blockers: (key) => matchingValueKeys(reverseHops(key), -1),

    hops,
    reverseHops
  })

  function hops (feedId) {
    if (!cache[feedId]) {
      var obs = cache[feedId] = MutantPullDict(() => {
        return api.sbot.pull.stream((sbot) => sbot.patchwork.contacts.hopStream({ feedId, live: true, max: 1, old: true }))
      }, {
        onListen: () => { cache[feedId] = obs },
        onUnlisten: () => delete cache[feedId],
        sync: true
      })
    }
    return cache[feedId]
  }

  function reverseHops (feedId) {
    if (!reverseCache[feedId]) {
      var obs = reverseCache[feedId] = MutantPullDict(() => {
        return api.sbot.pull.stream((sbot) => sbot.patchwork.contacts.hopStream({ feedId, live: true, old: true, max: 1, reverse: true }))
      }, {
        onListen: () => { reverseCache[feedId] = obs },
        onUnlisten: () => delete reverseCache[feedId],
        sync: true
      })
    }
    return reverseCache[feedId]
  }

  function matchingValueKeys (state, value) {
    var obs = computed(state, state => {
      return Object.keys(state).filter(key => {
        return state[key] === value
      })
    })

    obs.sync = state.sync
    return obs
  }
}
<commit_msg>remove debug code in contact/obs<commit_after>'use strict'

var nest = require('depnest')
var { computed } = require('mutant')
var MutantPullDict = require('../../lib/mutant-pull-dict')

exports.needs = nest({
  'sbot.pull.stream': 'first'
})

exports.gives = nest({
  'contact.obs': ['following', 'followers', 'blocking', 'blockers', 'hops', 'reverseHops']
})

exports.create = function (api) {
  var cache = {}
  var reverseCache = {}

  return nest('contact.obs', {
    following: (key) => matchingValueKeys(hops(key), 1),
    followers: (key) => matchingValueKeys(reverseHops(key), 1),
    blocking: (key) => matchingValueKeys(hops(key), -1),
    blockers: (key) => matchingValueKeys(reverseHops(key), -1),

    hops,
    reverseHops
  })

  function hops (feedId) {
    if (!cache[feedId]) {
      var obs = cache[feedId] = MutantPullDict(() => {
        return api.sbot.pull.stream((sbot) => sbot.patchwork.contacts.hopStream({ feedId, live: true, max: 1, old: true }))
      }, {
        onListen: () => { cache[feedId] = obs },
        onUnlisten: () => delete cache[feedId],
        sync: true
      })
    }
    return cache[feedId]
  }

  function reverseHops (feedId) {
    if (!reverseCache[feedId]) {
      var obs = reverseCache[feedId] = MutantPullDict(() => {
        return api.sbot.pull.stream((sbot) => sbot.patchwork.contacts.hopStream({ feedId, live: true, old: true, max: 1, reverse: true }))
      }, {
        onListen: () => { reverseCache[feedId] = obs },
        onUnlisten: () => delete reverseCache[feedId],
        sync: true
      })
    }
    return reverseCache[feedId]
  }

  function matchingValueKeys (state, value) {
    var obs = computed(state, state => {
      return Object.keys(state).filter(key => {
        return state[key] === value
      })
    })

    obs.sync = state.sync
    return obs
  }
}
<|endoftext|>


<commit_before>// Worker for E2EE/Insertable streams. Currently served as an inline blob.
const code = `
    // We use a ringbuffer of keys so we can change them and still decode packets that were
    // encrypted with an old key.
    // In the future when we dont rely on a globally shared key we will actually use it. For
    // now set the size to 1 which means there is only a single key. This causes some
    // glitches when changing the key but its ok.
    const keyRingSize = 1;

    // We use a 96 bit IV for AES GCM. This is signalled in plain together with the
    // packet. See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
    const ivLength = 12;

    // We copy the first bytes of the VP8 payload unencrypted.
    // For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
    //   https://tools.ietf.org/html/rfc6386#section-9.1
    // This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
    // and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
    // instead of being unable to decode).
    // This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
    //
    // For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
    //   https://tools.ietf.org/html/rfc6716#section-3.1
    const unencryptedBytes = {
        key: 10,
        delta: 3,
        undefined: 1 // frame.type is not set on audio
    };

    // Salt used in key derivation
    // FIXME: We currently use the MUC room name for this which has the same lifetime
    // as this worker. While not (pseudo)random as recommended in
    // https://developer.mozilla.org/en-US/docs/Web/API/Pbkdf2Params
    // this is easily available and the same for all participants.
    // We currently do not enforce a minimum length of 16 bytes either.
    let keySalt;

    // Raw keyBytes used to derive the key.
    let keyBytes;

    /**
     * Derives a AES-GCM key with 128 bits from the input using PBKDF2
     * @param {Uint8Array} keyBytes - Value to derive key from
     * @param {Uint8Array} salt - Salt used in key derivation
     */
    async function deriveKey(keyBytes, salt) {
        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
        const material = await crypto.subtle.importKey('raw', keyBytes,
            'PBKDF2', false, [ 'deriveBits', 'deriveKey' ]);

        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#PBKDF2
        return crypto.subtle.deriveKey({
            name: 'PBKDF2',
            salt,
            iterations: 100000,
            hash: 'SHA-256'
        }, material, {
            name: 'AES-GCM',
            length: 128
        }, false, [ 'encrypt', 'decrypt' ]);
    }

    /** Per-participant context holding the cryptographic keys and
     * encode/decode functions
     */
    class Context {
        /**
         * @param {string} id - local muc resourcepart
         */
        constructor(id) {
            // An array (ring) of keys that we use for sending and receiving.
            this._cryptoKeyRing = new Array(keyRingSize);

            // A pointer to the currently used key.
            this._currentKeyIndex = -1;

            // We keep track of how many frames we have sent per ssrc.
            // Starts with a random offset similar to the RTP sequence number.
            this._sendCounts = new Map();

            this._id = id;
        }

        /**
         * Sets a key and starts using it for encrypting.
         * @param {CryptoKey} key
         */
        setKey(key) {
            this._currentKeyIndex++;
            this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length] = key;
        }

        /**
         * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
         * https://tools.ietf.org/html/rfc7714#section-8.1
         * It concatenates
         * - the 32 bit synchronization source (SSRC) given on the encoded frame,
         * - the 32 bit rtp timestamp given on the encoded frame,
         * - a send counter that is specific to the SSRC. Starts at a random number.
         * The send counter is essentially the pictureId but we currently have to implement this ourselves.
         * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
         * randomly generated and SFUs may not rewrite this is considered acceptable.
         * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
         *   https://tools.ietf.org/html/rfc3711#section-4.1.1
         * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
         * opus audio) every second. For video it rolls over roughly every 13 hours.
         * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
         * every second. It will take a long time to roll over.
         *
         * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
         */
        makeIV(synchronizationSource, timestamp) {
            const iv = new ArrayBuffer(ivLength);
            const ivView = new DataView(iv);

            // having to keep our own send count (similar to a picture id) is not ideal.
            if (!this._sendCounts.has(synchronizationSource)) {
                // Initialize with a random offset, similar to the RTP sequence number.
                this._sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
            }
            const sendCount = this._sendCounts.get(synchronizationSource);

            ivView.setUint32(0, synchronizationSource);
            ivView.setUint32(4, timestamp);
            ivView.setUint32(8, sendCount % 0xFFFF);

            this._sendCounts.set(synchronizationSource, sendCount + 1);

            return iv;
        }

        /**
         * Function that will be injected in a stream and will encrypt the given encoded frames.
         *
         * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
         * @param {TransformStreamDefaultController} controller - TransportStreamController.
         *
         * The packet format is described below. One of the design goals was to not require
         * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
         * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
         * solve this eventually). This also "hides" that a client is using E2EE a bit.
         *
         * Note that this operates on the full frame, i.e. for VP8 the data described in
         *   https://tools.ietf.org/html/rfc6386#section-9.1
         *
         * The VP8 payload descriptor described in
         *   https://tools.ietf.org/html/rfc7741#section-4.2
         * is part of the RTP packet and not part of the frame and is not controllable by us.
         * This is fine as the SFU keeps having access to it for routing.
         *
         * The encrypted frame is formed as follows:
         * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
         * 2) Form the GCM IV for the frame as described above.
         * 3) Encrypt the rest of the frame using AES-GCM.
         * 4) Allocate space for the encrypted frame.
         * 5) Copy the unencrypted bytes to the start of the encrypted frame.
         * 6) Append the ciphertext to the encrypted frame.
         * 7) Append the IV.
         * 8) Append a single byte for the key identifier. TODO: we don't need all the bits.
         * 9) Enqueue the encrypted frame for sending.
         */
        encodeFunction(encodedFrame, controller) {
            const keyIndex = this._currentKeyIndex % this._cryptoKeyRing.length;

            if (this._cryptoKeyRing[keyIndex]) {
                const iv = this.makeIV(encodedFrame.synchronizationSource, encodedFrame.timestamp);

                return crypto.subtle.encrypt({
                    name: 'AES-GCM',
                    iv,
                    additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
                }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data,
                    unencryptedBytes[encodedFrame.type]))
                .then(cipherText => {
                    const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + cipherText.byteLength
                        + iv.byteLength + 1);
                    const newUint8 = new Uint8Array(newData);

                    newUint8.set(
                        new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])); // copy first bytes.
                    newUint8.set(
                        new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
                    newUint8.set(
                        new Uint8Array(iv), unencryptedBytes[encodedFrame.type] + cipherText.byteLength); // append IV.
                    newUint8[unencryptedBytes[encodedFrame.type] + cipherText.byteLength + ivLength]
                        = keyIndex; // set key index.

                    encodedFrame.data = newData;

                    return controller.enqueue(encodedFrame);
                }, e => {
                    console.error(e);

                    // We are not enqueuing the frame here on purpose.
                });
            }

            /* NOTE WELL:
             * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
             * This is ok for demo purposes but should not be done once this becomes more relied upon.
             */
            controller.enqueue(encodedFrame);
        }

        /**
         * Function that will be injected in a stream and will decrypt the given encoded frames.
         *
         * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
         * @param {TransformStreamDefaultController} controller - TransportStreamController.
         *
         * The decrypted frame is formed as follows:
         * 1) Extract the key index from the last byte of the encrypted frame.
         *    If there is no key associated with the key index, the frame is enqueued for decoding
         *    and these steps terminate.
         * 2) Determine the frame type in order to look up the number of unencrypted header bytes.
         * 2) Extract the 12-byte IV from its position near the end of the packet.
         *    Note: the IV is treated as opaque and not reconstructed from the input.
         * 3) Decrypt the encrypted frame content after the unencrypted bytes using AES-GCM.
         * 4) Allocate space for the decrypted frame.
         * 5) Copy the unencrypted bytes from the start of the encrypted frame.
         * 6) Append the plaintext to the decrypted frame.
         * 7) Enqueue the decrypted frame for decoding.
         */
        decodeFunction(encodedFrame, controller) {
            const data = new Uint8Array(encodedFrame.data);
            const keyIndex = data[encodedFrame.data.byteLength - 1];

            if (this._cryptoKeyRing[keyIndex]) {
                const iv = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - ivLength - 1, ivLength);
                const cipherTextStart = unencryptedBytes[encodedFrame.type];
                const cipherTextLength = encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
                    + ivLength + 1);

                return crypto.subtle.decrypt({
                    name: 'AES-GCM',
                    iv,
                    additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
                }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength))
                .then(plainText => {
                    const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
                    const newUint8 = new Uint8Array(newData);

                    newUint8.set(new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]));
                    newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);

                    encodedFrame.data = newData;

                    return controller.enqueue(encodedFrame);
                }, e => {
                    console.error(e);

                    // TODO: notify the application about error status.

                    // TODO: For video we need a better strategy since we do not want to based any
                    // non-error frames on a garbage keyframe.
                    if (encodedFrame.type === undefined) { // audio, replace with silence.
                        // audio, replace with silence.
                        const newData = new ArrayBuffer(3);
                        const newUint8 = new Uint8Array(newData);

                        newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
                        encodedFrame.data = newData;
                        controller.enqueue(encodedFrame);
                    }
                });
            } else if (keyIndex >= this._cryptoKeyRing.length
                    && this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length]) {
                // If we are encrypting but don't have a key for the remote drop the frame.
                // This is a heuristic since we don't know whether a packet is encrypted,
                // do not have a checksum and do not have signaling for whether a remote participant does
                // encrypt or not.
                return;
            }

            // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
            // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
            controller.enqueue(encodedFrame);
        }
    }
    const contexts = new Map(); // Map participant id => context

    onmessage = async event => {
        const { operation } = event.data;

        if (operation === 'initialize') {
            keySalt = event.data.salt;
        } else if (operation === 'encode') {
            const { readableStream, writableStream, participantId } = event.data;

            if (!contexts.has(participantId)) {
                contexts.set(participantId, new Context(participantId));
            }
            const context = contexts.get(participantId);
            const transformStream = new TransformStream({
                transform: context.encodeFunction.bind(context)
            });

            readableStream
                .pipeThrough(transformStream)
                .pipeTo(writableStream);
            if (keyBytes) {
                context.setKey(await deriveKey(keyBytes, keySalt));
            }
        } else if (operation === 'decode') {
            const { readableStream, writableStream, participantId } = event.data;

            if (!contexts.has(participantId)) {
                contexts.set(participantId, new Context(participantId));
            }
            const context = contexts.get(participantId);
            const transformStream = new TransformStream({
                transform: context.decodeFunction.bind(context)
            });

            readableStream
                .pipeThrough(transformStream)
                .pipeTo(writableStream);
            if (keyBytes) {
                context.setKey(await deriveKey(keyBytes, keySalt));
            }
        } else if (operation === 'setKey') {
            keyBytes = event.data.key;
            contexts.forEach(async context => {
                if (keyBytes) {
                    context.setKey(await deriveKey(keyBytes, keySalt));
                } else {
                    context.setKey(false);
                }
            });
        } else {
            console.error('e2ee worker', operation);
        }
    };
`;

export const createWorkerScript = () => URL.createObjectURL(new Blob([ code ], { type: 'application/javascript' }));
<commit_msg>e2ee: make it easier to change the AES key length<commit_after>// Worker for E2EE/Insertable streams. Currently served as an inline blob.
const code = `
    // We use a ringbuffer of keys so we can change them and still decode packets that were
    // encrypted with an old key.
    // In the future when we dont rely on a globally shared key we will actually use it. For
    // now set the size to 1 which means there is only a single key. This causes some
    // glitches when changing the key but its ok.
    const keyRingSize = 1;

    // We use a 96 bit IV for AES GCM. This is signalled in plain together with the
    // packet. See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
    const ivLength = 12;

    // We use a 128 bit key for AES GCM.
    const keyGenParameters = {
        name: 'AES-GCM',
        length: 128
    };

    // We copy the first bytes of the VP8 payload unencrypted.
    // For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
    //   https://tools.ietf.org/html/rfc6386#section-9.1
    // This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
    // and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
    // instead of being unable to decode).
    // This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
    //
    // For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
    //   https://tools.ietf.org/html/rfc6716#section-3.1
    const unencryptedBytes = {
        key: 10,
        delta: 3,
        undefined: 1 // frame.type is not set on audio
    };

    // Salt used in key derivation
    // FIXME: We currently use the MUC room name for this which has the same lifetime
    // as this worker. While not (pseudo)random as recommended in
    // https://developer.mozilla.org/en-US/docs/Web/API/Pbkdf2Params
    // this is easily available and the same for all participants.
    // We currently do not enforce a minimum length of 16 bytes either.
    let keySalt;

    // Raw keyBytes used to derive the key.
    let keyBytes;

    /**
     * Derives a AES-GCM key from the input using PBKDF2
     * The key length can be configured above and should be either 128 or 256 bits.
     * @param {Uint8Array} keyBytes - Value to derive key from
     * @param {Uint8Array} salt - Salt used in key derivation
     */
    async function deriveKey(keyBytes, salt) {
        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
        const material = await crypto.subtle.importKey('raw', keyBytes,
            'PBKDF2', false, [ 'deriveBits', 'deriveKey' ]);

        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#PBKDF2
        return crypto.subtle.deriveKey({
            name: 'PBKDF2',
            salt,
            iterations: 100000,
            hash: 'SHA-256'
        }, material, keyGenParameters, false, [ 'encrypt', 'decrypt' ]);
    }

    /** Per-participant context holding the cryptographic keys and
     * encode/decode functions
     */
    class Context {
        /**
         * @param {string} id - local muc resourcepart
         */
        constructor(id) {
            // An array (ring) of keys that we use for sending and receiving.
            this._cryptoKeyRing = new Array(keyRingSize);

            // A pointer to the currently used key.
            this._currentKeyIndex = -1;

            // We keep track of how many frames we have sent per ssrc.
            // Starts with a random offset similar to the RTP sequence number.
            this._sendCounts = new Map();

            this._id = id;
        }

        /**
         * Sets a key and starts using it for encrypting.
         * @param {CryptoKey} key
         */
        setKey(key) {
            this._currentKeyIndex++;
            this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length] = key;
        }

        /**
         * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
         * https://tools.ietf.org/html/rfc7714#section-8.1
         * It concatenates
         * - the 32 bit synchronization source (SSRC) given on the encoded frame,
         * - the 32 bit rtp timestamp given on the encoded frame,
         * - a send counter that is specific to the SSRC. Starts at a random number.
         * The send counter is essentially the pictureId but we currently have to implement this ourselves.
         * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
         * randomly generated and SFUs may not rewrite this is considered acceptable.
         * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
         *   https://tools.ietf.org/html/rfc3711#section-4.1.1
         * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
         * opus audio) every second. For video it rolls over roughly every 13 hours.
         * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
         * every second. It will take a long time to roll over.
         *
         * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
         */
        makeIV(synchronizationSource, timestamp) {
            const iv = new ArrayBuffer(ivLength);
            const ivView = new DataView(iv);

            // having to keep our own send count (similar to a picture id) is not ideal.
            if (!this._sendCounts.has(synchronizationSource)) {
                // Initialize with a random offset, similar to the RTP sequence number.
                this._sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
            }
            const sendCount = this._sendCounts.get(synchronizationSource);

            ivView.setUint32(0, synchronizationSource);
            ivView.setUint32(4, timestamp);
            ivView.setUint32(8, sendCount % 0xFFFF);

            this._sendCounts.set(synchronizationSource, sendCount + 1);

            return iv;
        }

        /**
         * Function that will be injected in a stream and will encrypt the given encoded frames.
         *
         * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
         * @param {TransformStreamDefaultController} controller - TransportStreamController.
         *
         * The packet format is described below. One of the design goals was to not require
         * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
         * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
         * solve this eventually). This also "hides" that a client is using E2EE a bit.
         *
         * Note that this operates on the full frame, i.e. for VP8 the data described in
         *   https://tools.ietf.org/html/rfc6386#section-9.1
         *
         * The VP8 payload descriptor described in
         *   https://tools.ietf.org/html/rfc7741#section-4.2
         * is part of the RTP packet and not part of the frame and is not controllable by us.
         * This is fine as the SFU keeps having access to it for routing.
         *
         * The encrypted frame is formed as follows:
         * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
         * 2) Form the GCM IV for the frame as described above.
         * 3) Encrypt the rest of the frame using AES-GCM.
         * 4) Allocate space for the encrypted frame.
         * 5) Copy the unencrypted bytes to the start of the encrypted frame.
         * 6) Append the ciphertext to the encrypted frame.
         * 7) Append the IV.
         * 8) Append a single byte for the key identifier. TODO: we don't need all the bits.
         * 9) Enqueue the encrypted frame for sending.
         */
        encodeFunction(encodedFrame, controller) {
            const keyIndex = this._currentKeyIndex % this._cryptoKeyRing.length;

            if (this._cryptoKeyRing[keyIndex]) {
                const iv = this.makeIV(encodedFrame.synchronizationSource, encodedFrame.timestamp);

                return crypto.subtle.encrypt({
                    name: 'AES-GCM',
                    iv,
                    additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
                }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data,
                    unencryptedBytes[encodedFrame.type]))
                .then(cipherText => {
                    const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + cipherText.byteLength
                        + iv.byteLength + 1);
                    const newUint8 = new Uint8Array(newData);

                    newUint8.set(
                        new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])); // copy first bytes.
                    newUint8.set(
                        new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
                    newUint8.set(
                        new Uint8Array(iv), unencryptedBytes[encodedFrame.type] + cipherText.byteLength); // append IV.
                    newUint8[unencryptedBytes[encodedFrame.type] + cipherText.byteLength + ivLength]
                        = keyIndex; // set key index.

                    encodedFrame.data = newData;

                    return controller.enqueue(encodedFrame);
                }, e => {
                    console.error(e);

                    // We are not enqueuing the frame here on purpose.
                });
            }

            /* NOTE WELL:
             * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
             * This is ok for demo purposes but should not be done once this becomes more relied upon.
             */
            controller.enqueue(encodedFrame);
        }

        /**
         * Function that will be injected in a stream and will decrypt the given encoded frames.
         *
         * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
         * @param {TransformStreamDefaultController} controller - TransportStreamController.
         *
         * The decrypted frame is formed as follows:
         * 1) Extract the key index from the last byte of the encrypted frame.
         *    If there is no key associated with the key index, the frame is enqueued for decoding
         *    and these steps terminate.
         * 2) Determine the frame type in order to look up the number of unencrypted header bytes.
         * 2) Extract the 12-byte IV from its position near the end of the packet.
         *    Note: the IV is treated as opaque and not reconstructed from the input.
         * 3) Decrypt the encrypted frame content after the unencrypted bytes using AES-GCM.
         * 4) Allocate space for the decrypted frame.
         * 5) Copy the unencrypted bytes from the start of the encrypted frame.
         * 6) Append the plaintext to the decrypted frame.
         * 7) Enqueue the decrypted frame for decoding.
         */
        decodeFunction(encodedFrame, controller) {
            const data = new Uint8Array(encodedFrame.data);
            const keyIndex = data[encodedFrame.data.byteLength - 1];

            if (this._cryptoKeyRing[keyIndex]) {
                const iv = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - ivLength - 1, ivLength);
                const cipherTextStart = unencryptedBytes[encodedFrame.type];
                const cipherTextLength = encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
                    + ivLength + 1);

                return crypto.subtle.decrypt({
                    name: 'AES-GCM',
                    iv,
                    additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
                }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength))
                .then(plainText => {
                    const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
                    const newUint8 = new Uint8Array(newData);

                    newUint8.set(new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]));
                    newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);

                    encodedFrame.data = newData;

                    return controller.enqueue(encodedFrame);
                }, e => {
                    console.error(e);

                    // TODO: notify the application about error status.

                    // TODO: For video we need a better strategy since we do not want to based any
                    // non-error frames on a garbage keyframe.
                    if (encodedFrame.type === undefined) { // audio, replace with silence.
                        // audio, replace with silence.
                        const newData = new ArrayBuffer(3);
                        const newUint8 = new Uint8Array(newData);

                        newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
                        encodedFrame.data = newData;
                        controller.enqueue(encodedFrame);
                    }
                });
            } else if (keyIndex >= this._cryptoKeyRing.length
                    && this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length]) {
                // If we are encrypting but don't have a key for the remote drop the frame.
                // This is a heuristic since we don't know whether a packet is encrypted,
                // do not have a checksum and do not have signaling for whether a remote participant does
                // encrypt or not.
                return;
            }

            // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
            // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
            controller.enqueue(encodedFrame);
        }
    }
    const contexts = new Map(); // Map participant id => context

    onmessage = async event => {
        const { operation } = event.data;

        if (operation === 'initialize') {
            keySalt = event.data.salt;
        } else if (operation === 'encode') {
            const { readableStream, writableStream, participantId } = event.data;

            if (!contexts.has(participantId)) {
                contexts.set(participantId, new Context(participantId));
            }
            const context = contexts.get(participantId);
            const transformStream = new TransformStream({
                transform: context.encodeFunction.bind(context)
            });

            readableStream
                .pipeThrough(transformStream)
                .pipeTo(writableStream);
            if (keyBytes) {
                context.setKey(await deriveKey(keyBytes, keySalt));
            }
        } else if (operation === 'decode') {
            const { readableStream, writableStream, participantId } = event.data;

            if (!contexts.has(participantId)) {
                contexts.set(participantId, new Context(participantId));
            }
            const context = contexts.get(participantId);
            const transformStream = new TransformStream({
                transform: context.decodeFunction.bind(context)
            });

            readableStream
                .pipeThrough(transformStream)
                .pipeTo(writableStream);
            if (keyBytes) {
                context.setKey(await deriveKey(keyBytes, keySalt));
            }
        } else if (operation === 'setKey') {
            keyBytes = event.data.key;
            contexts.forEach(async context => {
                if (keyBytes) {
                    context.setKey(await deriveKey(keyBytes, keySalt));
                } else {
                    context.setKey(false);
                }
            });
        } else {
            console.error('e2ee worker', operation);
        }
    };
`;

export const createWorkerScript = () => URL.createObjectURL(new Blob([ code ], { type: 'application/javascript' }));
<|endoftext|>


<commit_before>var EXPORTED_SYMBOLS = ['parse'];

Components.utils.import('resource://greasemonkey/script.js');
Components.utils.import('resource://greasemonkey/scriptIcon.js');
Components.utils.import('resource://greasemonkey/scriptRequire.js');
Components.utils.import('resource://greasemonkey/scriptResource.js');
Components.utils.import('resource://greasemonkey/third-party/MatchPattern.js');
Components.utils.import('resource://greasemonkey/util.js');

var gLineSplitRegexp = /.+/g;
var gAllMetaRegexp = new RegExp(
    '^// ==UserScript==([\\s\\S]*?)// ==/UserScript==', 'm');
var gMetaLineRegexp = new RegExp('// @(\\S+)(?:\\s+(.*))?');

/** Parse the source of a script; produce Script object. */
function parse(aSource, aUri) {
  var script = new Script();

  if (aUri) script._downloadURL = aUri.spec;
  if (aUri && aUri.spec) {
    var name = aUri.spec;
    name = name.substring(0, name.indexOf(".user.js"));
    name = name.substring(name.lastIndexOf("/") + 1);
    script._name = name;
  }
  if (aUri) script._namespace = aUri.host;

  var meta = aSource.match(gAllMetaRegexp);
  meta = meta && meta[1] || '';
  meta = meta.match(gLineSplitRegexp);

  var resourceNames = {};
  for (var i = 0, metaLine = ''; metaLine = meta[i]; i++) {
    metaLine = metaLine.replace(/\s+$/, '');

    var match = metaLine.match(gMetaLineRegexp);
    if (!match) continue;

    var header = match[1];
    var value = match[2] || null;

    switch (header) {
    case 'description':
    case 'name':
    case 'namespace':
    case 'updateURL':
    case 'version':
      script['_' + header] = value;
      break;

    case 'downloadURL':
    case 'installURL':
      script._downloadURL = value;
      break;
    case 'exclude':
      script._excludes.push(value);
      break;
    case 'icon':
      try {
        script.icon.setMetaVal(value);
        script._rawMeta += header + '\0' + value + '\0';
      } catch (e) {
        script.parseErrors.push(e.message);
      }
      break;
    case 'include':
      script._includes.push(value);
      break;
    case 'match':
      try {
        var match = new MatchPattern(value);
        script._matches.push(match);
      } catch (e) {
        // TODO: Localize.
        script.parseErrors.push(
            'Ignoring @match pattern ' + value + ' because:\n' + e);
      }
      break;
    case 'require':
      try {
        var reqUri = GM_util.uriFromUrl(value, aUri);
        var scriptRequire = new ScriptRequire(script);
        scriptRequire._downloadURL = reqUri.spec;
        script._requires.push(scriptRequire);
        script._rawMeta += header + '\0' + value + '\0';
      } catch (e) {
        // TODO: Localize.
        script.parseErrors.push('Failed to @require URL: '+ value);
      }
      break;
    case 'resource':
      var res = value.match(/(\S+)\s+(.*)/);
      if (res === null) {
        // TODO: Localize.
        script.parseErrors.push(
            'Invalid syntax for @resource declaration "' + value
            + '". Resources are declared like "@resource <name> <url>".');
        break;
      }

      var resName = res[1];
      if (resourceNames[resName]) {
        script.parseErrors.push(
            'Duplicate resource name "' + resName + '" detected. '
            + 'Each resource must have a unique name.');
        break;
      }
      resourceNames[resName] = true;

      try {
        var resUri = GM_util.uriFromUrl(res[2], aUri);
        var scriptResource = new ScriptResource(script);
        scriptResource._name = resName;
        scriptResource._downloadURL = resUri.spec;
        script._resources.push(scriptResource);
        script._rawMeta += header + '\0' + resName + '\0' + resUri.spec + '\0';
      } catch (e) {
        script.parseErrors.push(
            'Failed to get @resource '+ resName +' from ' + res[2]);
      }
      break;
    case 'run-at':
      script._runAt = value;
      break;
    case 'unwrap':
      script._unwrap = true;
      break;
    }
  }

  if (!script.updateURL && script._downloadURL) {
    script.updateURL = script._downloadURL;
  }
  if ('document-start' != script._runAt && 'document-end' != script._runAt) {
    script._runAt = 'document-end';
  }
  if (script._includes.length == 0 && script._matches.length == 0) {
    script._includes.push('*');
  }

  return script;
}
<commit_msg>"// ==/UserScript==" only ends script header at start of line.<commit_after>var EXPORTED_SYMBOLS = ['parse'];

Components.utils.import('resource://greasemonkey/script.js');
Components.utils.import('resource://greasemonkey/scriptIcon.js');
Components.utils.import('resource://greasemonkey/scriptRequire.js');
Components.utils.import('resource://greasemonkey/scriptResource.js');
Components.utils.import('resource://greasemonkey/third-party/MatchPattern.js');
Components.utils.import('resource://greasemonkey/util.js');

var gLineSplitRegexp = /.+/g;
var gAllMetaRegexp = new RegExp(
    '^// ==UserScript==([\\s\\S]*?)^// ==/UserScript==', 'm');
var gMetaLineRegexp = new RegExp('// @(\\S+)(?:\\s+(.*))?');

/** Parse the source of a script; produce Script object. */
function parse(aSource, aUri) {
  var script = new Script();

  if (aUri) script._downloadURL = aUri.spec;
  if (aUri && aUri.spec) {
    var name = aUri.spec;
    name = name.substring(0, name.indexOf(".user.js"));
    name = name.substring(name.lastIndexOf("/") + 1);
    script._name = name;
  }
  if (aUri) script._namespace = aUri.host;

  var meta = aSource.match(gAllMetaRegexp);
  meta = meta && meta[1] || '';
  meta = meta.match(gLineSplitRegexp);

  var resourceNames = {};
  for (var i = 0, metaLine = ''; metaLine = meta[i]; i++) {
    metaLine = metaLine.replace(/\s+$/, '');

    var match = metaLine.match(gMetaLineRegexp);
    if (!match) continue;

    var header = match[1];
    var value = match[2] || null;

    switch (header) {
    case 'description':
    case 'name':
    case 'namespace':
    case 'updateURL':
    case 'version':
      script['_' + header] = value;
      break;

    case 'downloadURL':
    case 'installURL':
      script._downloadURL = value;
      break;
    case 'exclude':
      script._excludes.push(value);
      break;
    case 'icon':
      try {
        script.icon.setMetaVal(value);
        script._rawMeta += header + '\0' + value + '\0';
      } catch (e) {
        script.parseErrors.push(e.message);
      }
      break;
    case 'include':
      script._includes.push(value);
      break;
    case 'match':
      try {
        var match = new MatchPattern(value);
        script._matches.push(match);
      } catch (e) {
        // TODO: Localize.
        script.parseErrors.push(
            'Ignoring @match pattern ' + value + ' because:\n' + e);
      }
      break;
    case 'require':
      try {
        var reqUri = GM_util.uriFromUrl(value, aUri);
        var scriptRequire = new ScriptRequire(script);
        scriptRequire._downloadURL = reqUri.spec;
        script._requires.push(scriptRequire);
        script._rawMeta += header + '\0' + value + '\0';
      } catch (e) {
        // TODO: Localize.
        script.parseErrors.push('Failed to @require URL: '+ value);
      }
      break;
    case 'resource':
      var res = value.match(/(\S+)\s+(.*)/);
      if (res === null) {
        // TODO: Localize.
        script.parseErrors.push(
            'Invalid syntax for @resource declaration "' + value
            + '". Resources are declared like "@resource <name> <url>".');
        break;
      }

      var resName = res[1];
      if (resourceNames[resName]) {
        script.parseErrors.push(
            'Duplicate resource name "' + resName + '" detected. '
            + 'Each resource must have a unique name.');
        break;
      }
      resourceNames[resName] = true;

      try {
        var resUri = GM_util.uriFromUrl(res[2], aUri);
        var scriptResource = new ScriptResource(script);
        scriptResource._name = resName;
        scriptResource._downloadURL = resUri.spec;
        script._resources.push(scriptResource);
        script._rawMeta += header + '\0' + resName + '\0' + resUri.spec + '\0';
      } catch (e) {
        script.parseErrors.push(
            'Failed to get @resource '+ resName +' from ' + res[2]);
      }
      break;
    case 'run-at':
      script._runAt = value;
      break;
    case 'unwrap':
      script._unwrap = true;
      break;
    }
  }

  if (!script.updateURL && script._downloadURL) {
    script.updateURL = script._downloadURL;
  }
  if ('document-start' != script._runAt && 'document-end' != script._runAt) {
    script._runAt = 'document-end';
  }
  if (script._includes.length == 0 && script._matches.length == 0) {
    script._includes.push('*');
  }

  return script;
}
<|endoftext|>


<commit_before>/**
 * @fileOverview provides functions to work with the Ringo shell.
 */

var system = require('system');
var term = require('ringo/term');

export('write', 'writeln', 'read', 'readln', 'start', 'quit',
       'printResult', 'printError');

try {
    var input = new Packages.jline.ConsoleReader();
} catch (x) {
    // fall back to plain standard input
}

/**
 * Write 0..n arguments to standard output.
 */
function write() {
    var length = arguments.length;
    var out = system.stdout;
    for (var i = 0; i < length; i++) {
        out.write(String(arguments[i]));
        if (i < length - 1)
            out.write(' ');
    }
}

/**
 * Write 0..n arguments to standard output, followed by a newline.
 */
function writeln() {
    write.apply(this, arguments);
    system.stdout.writeLine('');
}

/**
 * Read a single character from the standard input.
 */
function read() {
    if (!input) {
        throw new Error('jline not installed');
    }
    return String.fromCharCode(input.readVirtualKey());
}

/**
 * Read a single line from the standard input.
 * @param {String} prompt optional prompt to display
 * @param {String} echoChar character to use as echo,
 *         e.g. '*' for passwords or '' for no echo.
 */
function readln(prompt, echoChar) {
    prompt = prompt || '';
    if (input) {
        if (typeof echoChar == 'string') {
            var echo = echoChar == '' ?
                   new java.lang.Character(0) :
                   new java.lang.Character(echoChar.charCodeAt(0));
            return input.readLine(prompt, echo);
        }
        return input.readLine(prompt);
    } else {
        system.stdout.write(prompt);
        return system.stdin.readLine().trim();
    }
}

/**
 * Start the shell programmatically. This uses the current thread and thus will not
 * return. You should therefore call this function as the last statement in your script.
 * Terminating the shell will exit the program.
 * @since 0.5
 */
function start(engine) {
    engine = engine || require('ringo/engine').getRhinoEngine();
    new org.ringojs.tools.RingoShell(engine).run();
}

/**
 * Quit the shell and exit the JVM.
 * @param {Number} status optional integer exit status code (default is 0)
 */
function quit(status) {
   java.lang.System.exit(status || 0);
}

var styles = {
    'number': term.BLUE,
    'string': term.GREEN,
    'function': term.CYAN,
    'boolean': term.YELLOW,
    'null': term.BOLD,
    'date': term.MAGENTA,
    'java': term.MAGENTA,
    'custom': term.RED
}

function convert(value, nesting, visited) {
    var type = typeof value;
    var retval = {type: type};
    switch (type) {
        case 'number':
            retval.string = String(value);
            break;
        case 'string':
            retval.string  = "'" + value.replace(/'/g, "\\'") + "'";
            break;
        case 'function':
            retval.string = "[Function]";
            break;
        case 'boolean':
            retval.string =  String(value);
            break;
        case 'object':
            if (value === null) {
                retval.type = retval.string = 'null';
                break;
            }
            if (visited.indexOf(value) > -1) {
                retval.type = "cyclic";
                retval.string = "[CyclicRef]";
                break;
            }
            visited.push(value);
            if (value instanceof java.lang.Object && typeof value.getClass === "function") {
                retval.type = "java";
                retval.string = "[" + value.getClass().getName() + " "
                        + String(value) + "]";
            } else if (value instanceof Date) {
                retval.type = "date";
                retval.string = String(value);
            } else if (Array.isArray(value)) {
                if (nesting > 1) {
                    retval.string = "[...]";
                } else {
                    retval.type = "array";
                    retval.items = [];
                    var count = 0;
                    for (var i = 0; i < value.length; i++) {
                        var part = convert(value[i], nesting + 1, visited);
                        count += (part.string && part.string.length || part.count || 0) + 2;
                        retval.items.push(part);
                    }
                    retval.count = count;
                }
            } else if (value.toString !== Object.prototype.toString) {
                // if object provides its own toString we assume it wants to use it
                retval.type = "custom";
                retval.string = value.toString();
                break;
            } else {
                if (nesting > 1) {
                    retval.string = "{...}";
                } else {
                    retval.items = [];
                    var keys = Object.keys(value);
                    count = 0;
                    for (i = 0; i < keys.length; i++) {
                        part = convert(value[keys[i]], nesting + 1, visited);
                        count += String(keys[i]).length + 4;
                        count += part.string && part.string.length || part.count || 0;
                        retval.items.push({
                            key: keys[i] + ": ",
                            value: part
                        });
                    }
                    retval.count = count;
                }
            }
            break;
        case 'undefined':
            retval = {};
            break;
        default:
            retval.string = String(value);
            break;
    }
    return retval;
}

function printResult(value, writer) {
    if (typeof value !== "undefined") {
        writer = writer || term;
        printValue(convert(value, 0, []), writer, 0);
        writer.writeln();
    }
}

function printValue(value, writer, nesting) {
    if (value.string) {
        var style = styles[value.type] || "";
        writer.write(style + value.string + term.RESET);
    } else if (value && value.items) {
        var multiline = value.count > 60;
        var isArray = value.type === "array";
        var length = value.items.length;
        if (length === 0) {
            writer.write(isArray ? "[]" : "{}");
            return;
        }
        var opener = isArray ? "[" : "{";
        if (multiline && nesting > 0) {
            writer.write(opener + "\n  ");
            for (j = 0; j < nesting; j++)
                writer.write("  ");
        } else {
            writer.write(opener + " ");
        }
        if (isArray) {
            for (var i = 0; i < length; i++) {
                printValue(value.items[i], writer, nesting + 1);
                if (i < length - 1) {
                    if (multiline) {
                        writer.write(",\n  ");
                        for (var j = 0; j < nesting; j++)
                            writer.write("  ");
                    } else {
                        writer.write(", ");
                    }
                }
            }
            writer.write(term.RESET + " ]");
        } else {
            for (i = 0; i < length; i++) {
                writer.write(value.items[i].key);
                printValue(value.items[i].value, writer, nesting + 1);
                if (i < length - 1) {
                    if (multiline) {
                        writer.write(",\n  ");
                        for (j = 0; j < nesting; j++)
                            writer.write("  ");
                    } else {
                        writer.write(", ");
                    }
                }
            }
            writer.write(term.RESET + " }");
        }
    }
}

/**
 * @param {Exception} xcept
 * @param {Boolean} verbose
 */
function printError(xcept, errors, verbose) {
    if (xcept instanceof org.mozilla.javascript.RhinoException) {
        term.writeln(term.BOLD, term.RED, xcept.details());
    } else {
        term.writeln(term.BOLD, term.RED, xcept.toString());
    }
    for (var i = 0, l = errors.length; i < l; i++) {
        term.writeln(term.GREEN, errors[i]);
    }
    if (typeof xcept.getScriptStackTrace === "function") {
        term.write(xcept.getScriptStackTrace());
    }
    if (verbose) {
        if (typeof xcept.getWrappedException === "function") {
            xcept = xcept.getWrappedException();
        }
        term.writeln(term.BOLD, "Java Exception:")
        xcept.printStackTrace(system.stdout.raw || System.out);
    }
}
<commit_msg>[docs] Fixed typos in ringo/shell<commit_after>/**
 * @fileOverview Provides functions to work with the Ringo shell.
 */

var system = require('system');
var term = require('ringo/term');

export('write', 'writeln', 'read', 'readln', 'start', 'quit',
       'printResult', 'printError');

try {
    var input = new Packages.jline.ConsoleReader();
} catch (x) {
    // fall back to plain standard input
}

/**
 * Write 0..n arguments to standard output.
 */
function write() {
    var length = arguments.length;
    var out = system.stdout;
    for (var i = 0; i < length; i++) {
        out.write(String(arguments[i]));
        if (i < length - 1)
            out.write(' ');
    }
}

/**
 * Write 0..n arguments to standard output, followed by a newline.
 */
function writeln() {
    write.apply(this, arguments);
    system.stdout.writeLine('');
}

/**
 * Read a single character from the standard input.
 */
function read() {
    if (!input) {
        throw new Error('jline not installed');
    }
    return String.fromCharCode(input.readVirtualKey());
}

/**
 * Read a single line from the standard input.
 * @param {String} prompt optional prompt to display
 * @param {String} echoChar character to use as echo,
 *         e.g. '*' for passwords or '' for no echo.
 */
function readln(prompt, echoChar) {
    prompt = prompt || '';
    if (input) {
        if (typeof echoChar == 'string') {
            var echo = echoChar == '' ?
                   new java.lang.Character(0) :
                   new java.lang.Character(echoChar.charCodeAt(0));
            return input.readLine(prompt, echo);
        }
        return input.readLine(prompt);
    } else {
        system.stdout.write(prompt);
        return system.stdin.readLine().trim();
    }
}

/**
 * Start the shell programmatically. This uses the current thread and thus will not
 * return. You should therefore call this function as the last statement in your script.
 * Terminating the shell will exit the program.
 * @since 0.5
 */
function start(engine) {
    engine = engine || require('ringo/engine').getRhinoEngine();
    new org.ringojs.tools.RingoShell(engine).run();
}

/**
 * Quit the shell and exit the JVM.
 * @param {Number} status optional integer exit status code (default is 0)
 */
function quit(status) {
   java.lang.System.exit(status || 0);
}

var styles = {
    'number': term.BLUE,
    'string': term.GREEN,
    'function': term.CYAN,
    'boolean': term.YELLOW,
    'null': term.BOLD,
    'date': term.MAGENTA,
    'java': term.MAGENTA,
    'custom': term.RED
}

function convert(value, nesting, visited) {
    var type = typeof value;
    var retval = {type: type};
    switch (type) {
        case 'number':
            retval.string = String(value);
            break;
        case 'string':
            retval.string  = "'" + value.replace(/'/g, "\\'") + "'";
            break;
        case 'function':
            retval.string = "[Function]";
            break;
        case 'boolean':
            retval.string =  String(value);
            break;
        case 'object':
            if (value === null) {
                retval.type = retval.string = 'null';
                break;
            }
            if (visited.indexOf(value) > -1) {
                retval.type = "cyclic";
                retval.string = "[CyclicRef]";
                break;
            }
            visited.push(value);
            if (value instanceof java.lang.Object && typeof value.getClass === "function") {
                retval.type = "java";
                retval.string = "[" + value.getClass().getName() + " "
                        + String(value) + "]";
            } else if (value instanceof Date) {
                retval.type = "date";
                retval.string = String(value);
            } else if (Array.isArray(value)) {
                if (nesting > 1) {
                    retval.string = "[...]";
                } else {
                    retval.type = "array";
                    retval.items = [];
                    var count = 0;
                    for (var i = 0; i < value.length; i++) {
                        var part = convert(value[i], nesting + 1, visited);
                        count += (part.string && part.string.length || part.count || 0) + 2;
                        retval.items.push(part);
                    }
                    retval.count = count;
                }
            } else if (value.toString !== Object.prototype.toString) {
                // if object provides its own toString we assume it wants to use it
                retval.type = "custom";
                retval.string = value.toString();
                break;
            } else {
                if (nesting > 1) {
                    retval.string = "{...}";
                } else {
                    retval.items = [];
                    var keys = Object.keys(value);
                    count = 0;
                    for (i = 0; i < keys.length; i++) {
                        part = convert(value[keys[i]], nesting + 1, visited);
                        count += String(keys[i]).length + 4;
                        count += part.string && part.string.length || part.count || 0;
                        retval.items.push({
                            key: keys[i] + ": ",
                            value: part
                        });
                    }
                    retval.count = count;
                }
            }
            break;
        case 'undefined':
            retval = {};
            break;
        default:
            retval.string = String(value);
            break;
    }
    return retval;
}

function printResult(value, writer) {
    if (typeof value !== "undefined") {
        writer = writer || term;
        printValue(convert(value, 0, []), writer, 0);
        writer.writeln();
    }
}

function printValue(value, writer, nesting) {
    if (value.string) {
        var style = styles[value.type] || "";
        writer.write(style + value.string + term.RESET);
    } else if (value && value.items) {
        var multiline = value.count > 60;
        var isArray = value.type === "array";
        var length = value.items.length;
        if (length === 0) {
            writer.write(isArray ? "[]" : "{}");
            return;
        }
        var opener = isArray ? "[" : "{";
        if (multiline && nesting > 0) {
            writer.write(opener + "\n  ");
            for (j = 0; j < nesting; j++)
                writer.write("  ");
        } else {
            writer.write(opener + " ");
        }
        if (isArray) {
            for (var i = 0; i < length; i++) {
                printValue(value.items[i], writer, nesting + 1);
                if (i < length - 1) {
                    if (multiline) {
                        writer.write(",\n  ");
                        for (var j = 0; j < nesting; j++)
                            writer.write("  ");
                    } else {
                        writer.write(", ");
                    }
                }
            }
            writer.write(term.RESET + " ]");
        } else {
            for (i = 0; i < length; i++) {
                writer.write(value.items[i].key);
                printValue(value.items[i].value, writer, nesting + 1);
                if (i < length - 1) {
                    if (multiline) {
                        writer.write(",\n  ");
                        for (j = 0; j < nesting; j++)
                            writer.write("  ");
                    } else {
                        writer.write(", ");
                    }
                }
            }
            writer.write(term.RESET + " }");
        }
    }
}

/**
 * @param {Exception} xcept
 * @param {Array} errors
 * @param {Boolean} verbose
 */
function printError(xcept, errors, verbose) {
    if (xcept instanceof org.mozilla.javascript.RhinoException) {
        term.writeln(term.BOLD, term.RED, xcept.details());
    } else {
        term.writeln(term.BOLD, term.RED, xcept.toString());
    }
    for (var i = 0, l = errors.length; i < l; i++) {
        term.writeln(term.GREEN, errors[i]);
    }
    if (typeof xcept.getScriptStackTrace === "function") {
        term.write(xcept.getScriptStackTrace());
    }
    if (verbose) {
        if (typeof xcept.getWrappedException === "function") {
            xcept = xcept.getWrappedException();
        }
        term.writeln(term.BOLD, "Java Exception:")
        xcept.printStackTrace(system.stdout.raw || System.out);
    }
}
<|endoftext|>


<commit_before>/**
 * @fileOverview provides functions to work with the Ringo shell.
 */

var system = require('system');

export('write', 'writeln', 'read', 'readln', 'start', 'quit');

try {
    var input = new Packages.jline.ConsoleReader();
} catch (x) {
    // fall back to plain standard input
}

/**
 * Write 0..n arguments to standard output.
 */
function write() {
    var length = arguments.length;
    var out = system.stdout;
    for (var i = 0; i < length; i++) {
        out.write(String(arguments[i]));
        if (i < length - 1)
            out.write(' ');
    }
}

/**
 * Write 0..n arguments to standard output, followed by a newline.
 */
function writeln() {
    write.apply(this, arguments);
    system.stdout.writeLine('');
}

/**
 * Read a single character from the standard input.
 */
function read() {
    if (!input) {
        throw new Error('jline not installed');
    }
    return String.fromCharCode(input.readVirtualKey());
}

/**
 * Read a single line from the standard input.
 * @param prompt {String} optional prompt to display
 * @param echoChar {String} character to use as echo,
 *         e.g. '*' for passwords or '' for no echo.
 */
function readln(prompt, echoChar) {
    prompt = prompt || '';
    if (input) {
        if (typeof echoChar == 'string') {
            var echo = echoChar == '' ?
                   new java.lang.Character(0) :
                   new java.lang.Character(echoChar.charCodeAt(0));
            return input.readLine(prompt, echo);
        }
        return input.readLine(prompt);
    } else {
        system.stdout.write(prompt);
        return system.stdin.readLine().trim();
    }
}

/**
 * Start the shell programmatically. This uses the current thread and thus will not
 * return. You should therefore call this function as the last statement in your script.
 * Terminating the shell will exit the program.
 * @since 0.5
 */
function start(engine) {
    engine = engine || require('ringo/engine').getRhinoEngine();
    new org.ringojs.tools.RingoShell(engine).run();
}

/**
 * Quit the shell and exit the JVM.
 * @param status {Number} optional exit status (default is 0)
 */
function quit(status) {
   java.lang.System.exit(status | 0);
}
<commit_msg>Minor code cosmetics<commit_after>/**
 * @fileOverview provides functions to work with the Ringo shell.
 */

var system = require('system');

export('write', 'writeln', 'read', 'readln', 'start', 'quit');

try {
    var input = new Packages.jline.ConsoleReader();
} catch (x) {
    // fall back to plain standard input
}

/**
 * Write 0..n arguments to standard output.
 */
function write() {
    var length = arguments.length;
    var out = system.stdout;
    for (var i = 0; i < length; i++) {
        out.write(String(arguments[i]));
        if (i < length - 1)
            out.write(' ');
    }
}

/**
 * Write 0..n arguments to standard output, followed by a newline.
 */
function writeln() {
    write.apply(this, arguments);
    system.stdout.writeLine('');
}

/**
 * Read a single character from the standard input.
 */
function read() {
    if (!input) {
        throw new Error('jline not installed');
    }
    return String.fromCharCode(input.readVirtualKey());
}

/**
 * Read a single line from the standard input.
 * @param prompt {String} optional prompt to display
 * @param echoChar {String} character to use as echo,
 *         e.g. '*' for passwords or '' for no echo.
 */
function readln(prompt, echoChar) {
    prompt = prompt || '';
    if (input) {
        if (typeof echoChar == 'string') {
            var echo = echoChar == '' ?
                   new java.lang.Character(0) :
                   new java.lang.Character(echoChar.charCodeAt(0));
            return input.readLine(prompt, echo);
        }
        return input.readLine(prompt);
    } else {
        system.stdout.write(prompt);
        return system.stdin.readLine().trim();
    }
}

/**
 * Start the shell programmatically. This uses the current thread and thus will not
 * return. You should therefore call this function as the last statement in your script.
 * Terminating the shell will exit the program.
 * @since 0.5
 */
function start(engine) {
    engine = engine || require('ringo/engine').getRhinoEngine();
    new org.ringojs.tools.RingoShell(engine).run();
}

/**
 * Quit the shell and exit the JVM.
 * @param status {Number} optional integer exit status code (default is 0)
 */
function quit(status) {
   java.lang.System.exit(status || 0);
}
<|endoftext|>


<commit_before>/*! Natural Compare v1.1.0 | (c) 2015 Nathan Woltman | https://github.com/woollybogger/string-natural-compare */
!function(){"use strict";function a(a){return a>=48&&57>=a}function b(b,c){for(var f,g,h=(b+="").length,i=(c+="").length,j=0,k=0;h>j&&i>k;){var l=b.charCodeAt(j),m=c.charCodeAt(k);if(a(l)){if(!a(m))return l-m;for(var n=j,o=k;48===l&&++n<h;)l=b.charCodeAt(n);for(;48===m&&++o<i;)m=c.charCodeAt(o);for(var p=n,q=o;h>p&&a(b.charCodeAt(p));)++p;for(;i>q&&a(c.charCodeAt(q));)++q;var r=p-n,s=q-o;if(s>r)return-1;if(r>s)return 1;if(r){var t=b.slice(n,p),u=c.slice(o,q);if(u>t)return-1;if(t>u)return 1}j=p,k=q}else{if(l!==m)return e&&e>l&&e>m&&-1!==(f=d[l])&&-1!==(g=d[m])?f-g:l-m;++j,++k}}return h-i}var c,d,e=0;Object.defineProperties(String,{alphabet:{get:function(){return c},set:function(a){c=a,d=[];var b=0;if(c)for(;b<c.length;b++)d[c.charCodeAt(b)]=b;for(e=d.length,b=0;e>b;b++)b in d||(d[b]=-1)}},naturalCompare:{value:b,configurable:!0,writable:!0},naturalCaseCompare:{value:function(a,c){return b((""+a).toLowerCase(),(""+c).toLowerCase())},configurable:!0,writable:!0}})}();<commit_msg>Fix version number in natural-compare.min.js<commit_after>/*! Natural Compare v1.1.1 | (c) 2015 Nathan Woltman | https://github.com/woollybogger/string-natural-compare */
!function(){"use strict";function a(a){return a>=48&&57>=a}function b(b,c){for(var f,g,h=(b+="").length,i=(c+="").length,j=0,k=0;h>j&&i>k;){var l=b.charCodeAt(j),m=c.charCodeAt(k);if(a(l)){if(!a(m))return l-m;for(var n=j,o=k;48===l&&++n<h;)l=b.charCodeAt(n);for(;48===m&&++o<i;)m=c.charCodeAt(o);for(var p=n,q=o;h>p&&a(b.charCodeAt(p));)++p;for(;i>q&&a(c.charCodeAt(q));)++q;var r=p-n,s=q-o;if(s>r)return-1;if(r>s)return 1;if(r){var t=b.slice(n,p),u=c.slice(o,q);if(u>t)return-1;if(t>u)return 1}j=p,k=q}else{if(l!==m)return e&&e>l&&e>m&&-1!==(f=d[l])&&-1!==(g=d[m])?f-g:l-m;++j,++k}}return h-i}var c,d,e=0;Object.defineProperties(String,{alphabet:{get:function(){return c},set:function(a){c=a,d=[];var b=0;if(c)for(;b<c.length;b++)d[c.charCodeAt(b)]=b;for(e=d.length,b=0;e>b;b++)b in d||(d[b]=-1)}},naturalCompare:{value:b,configurable:!0,writable:!0},naturalCaseCompare:{value:function(a,c){return b((""+a).toLowerCase(),(""+c).toLowerCase())},configurable:!0,writable:!0}})}();<|endoftext|>


<commit_before>var fs = require('fs'),
	sys = require('sys'),
	path = require('path'),
	util = require('./util'),
	bind = util.bind,
	map = util.map,
	repeat = util.repeat,
	boxComment = util.boxComment,
	q = util.q,
	tokenizer = require('./tokenizer'),
	parser = require('./parser'),
	compiler = exports,
	gModules = {}

exports.compile = util.intercept('CompileError', function (ast, context) {

	context = context || { hookName: name('ROOT_HOOK'), referenceTable: {} } // root context

	return code(
		';(function funApp() {',
		'	var {{ hookName }} = fun.name("rootHook")',
		'	fun.setHook({{ hookName }}, document.body)',
		'	{{ code }}',
		'	{{ modules }}',
		'})(); // let\'s kick it',
		{
			hookName: context.hookName,
			code: compile(context, ast),
			modules: map(gModules, function(module, name) {
				return boxComment('Module: ' + name) + '\n' + module.jsCode }).join('\n\n\n')
		})
})

/************************
 * Top level statements *
 ************************/
function compile(context, ast) {
	assert(context && context.hookName && context.referenceTable, ast, "compile called with invalid context")
	if (ast instanceof Array) {
		return map(ast, bind(this, compileStatement, context)).join('\n') + '\n'
	} else {
		return compileStatement(context, ast) + '\n'
	}
}

function compileStatement(context, ast) {
	switch (ast.type) {
		case 'STATIC_VALUE':    return compileStaticValue(context, ast)
		case 'ALIAS':           return compileStatement(context, resolve(context, ast))
		case 'ITEM_PROPERTY':   return compileItemProperty(context, ast)
		case 'XML':             return compileXML(context, ast)
		case 'DECLARATION':     return compileDeclaration(context, ast)
		case 'IF_STATEMENT':    return compileIfStatement(context, ast)
		case 'FOR_LOOP':        return compileForLoop(context, ast)
		case 'INVOCATION':      return compileInvocation(context, ast)
		case 'IMPORT_MODULE':   return compileModuleImport(context, ast)
		case 'IMPORT_FILE':     return compileFileImport(context, ast)
		default:                halt(ast, 'Unknown AST type ' + ast.type)
	}
}

/*****************
 * Static values *
 *****************/
function compileStaticValue(context, ast) {
	return code(
		'fun.hook({{ parentHook }}, fun.name("inlineString")).innerHTML = {{ value }}',
		{
			parentHook: context.hookName,
			value: JSON.stringify(ast.value)
		})
}

/************************
 * Item Property values *
 ************************/
function compileItemProperty(context, ast) {
	assert(ast.property.length > 0, ast, 'Missing property on item reference. "'+ast.namespace[0]+'" should probably be something like "'+ast.namespace[0]+'.foo"')
	assert(ast.property.length == 1, ast, 'TODO: Handle nested property references')
	var hookName = name('ITEM_PROPERTY_HOOK')
	return code(
		'fun.hook({{ parentHook }}, {{ hookName }})',
		'fun.observe({{ type }}, {{ id }}, {{ property }}, function(mutation, value) {',
		'	fun.getHook({{ hookName }}).innerHTML = value',
		'})',
		{
			parentHook: context.hookName,
			hookName: q(hookName),
			id: q(ast.item.id),
			property: q(ast.property[0]),
			type: q('BYTES')
		})
}

/*******
 * XML *
 *******/
function compileXML(context, ast) {
	var hookName = name('XML_HOOK'),
		newContext = util.shallowCopy(context, { hookName:hookName })

	var attributes = _handleXMLAttributes(context, ast, hookName)
	return code(
		'var {{ hookName }} = fun.name()',
		'fun.hook({{ parentHook }}, {{ hookName }}, {{ tagName }}, {{ staticAttributes }})',
		'{{ dynamicAttributesCode }}',
		'{{ childCode }}',
		{
			parentHook: context.hookName,
			hookName: hookName,
			tagName: q(ast.tag),
			staticAttributes: q(attributes.staticAttributes),
			dynamicAttributesCode: attributes.dynamicCode,
			childCode: ast.block ? compile(newContext, ast.block) : ''
		})
}

function _handleXMLAttributes(context, ast, hookName) {
	var staticAttributes = {},
		dynamicCode = []
	for (var i=0, attribute; attribute = ast.attributes[i]; i++) {
		assert(attribute.namespace.length == 1, ast, 'TODO Handle dot notation XML attribute namespace (for e.g. style.width=100)')
		var name = attribute.namespace[0],
			value = resolve(context, attribute.value),
			match
		if (name == 'style') {
			assert(value.type == 'NESTED_ALIAS', ast, 'You can only assign the style attribute to a JSON object literal, e.g.
')
			_handleStyleAttribute(staticAttributes, dynamicCode, context, ast, hookName, value.content)
		} else if (match = name.match(/^on(\w+)$/)) {
			halt('TODO Handle on* xml attributes')
		} else if (value.type == 'STATIC_VALUE') {
			staticAttributes[name] = value.value
		} else {
			assert(value.type != 'NESTED_ALIAS', ast, 'Does not make sense to assign a JSON object literal to other attribtues than "style" (tried to assign to "'+name+'")')
			_handleDynamicAttribute(dynamicCode, context, ast, hookName, name, value)
		}
	}
	return { staticAttributes: staticAttributes, dynamicCode: dynamicCode.join('\n') }
}

// add static attributes to staticAttrs object
function _handleStyleAttribute(staticAttrs, dynamicCode, context, ast, hookName, styles) {
	var styleAttribute = staticAttrs['style'] = {}
	for (var i=0, style; style = styles[i]; i++) {
		var value = resolve(context, style.value)
		if (value.type == 'STATIC_VALUE') {
			styleAttribute[style.name] = value.value
		} else {
			_handleDynamicAttribute(dynamicCode, context, ast, hookName, 'style.' + style.name, value)
		}
	}
}

// add code for dynamic properties to dynamicCode
function _handleDynamicAttribute(dynamicCode, context, ast, hookName, attrName, value) {
	assert(value.property.length == 1, ast, 'TODO: Handle nested item property references')
	dynamicCode.push(code(
		'fun.observe({{ type }}, {{ id }}, {{ property }}, function(mutation, value) {',
		'	fun.attr({{ hookName }}, {{ attr }}, value)',
		'})',
		{
			type: q('BYTES'),
			id: q(value.item.id),
			property: q(value.property[0]),
			attr: q(attrName),
			hookName: hookName
		}))
}

/*************************
 * Module & File Imports *
 *************************/
function compileModuleImport(context, ast) {
	if (gModules[ast.name]) { return }
	var module = gModules[ast.name] = {
		name: ast.name,
		path: __dirname + '/modules/' + ast.name + '/'
	}
	assert(fs.statSync(module.path).isDirectory(), ast, 'Could not find the module at ' + module.path)
	// TODO Read a package/manifest.json file in the module directory, describing name/version/which files to load, etc
	if (fs.statSync(module.path + 'lib.fun').isFile()) {
		var result = _importFile(module.path + 'lib.fun', context)
	}
	if (path.existsSync(module.path + 'lib.js')) {
		module.jsCode = fs.readFileSync(module.path + 'lib.js')
	} else {
		module.jsCode = '// No JS code for ' + module.name
	}
	return result
}

function compileFileImport(context, ast) {
	var filePath = __dirname + '/' + ast.path + '.fun'
	assert(path.existsSync(filePath), ast, 'Could not find file for import: "'+filePath+'"')
	return _importFile(filePath, context)
}

function _importFile(path, context) {
	var tokens = tokenizer.tokenize(path)
	var newAST = parser.parse(tokens)
	return compile(context, newAST)
}

/**********************
 * Alias Declarations *
 **********************/

function compileDeclaration(context, ast) {
	_storeAlias(context, ast)
	return ''
}

var _storeAlias = function(context, ast) {
	var baseValue = _getAlias(context, ast, true),
		namespace = ast.namespace,
		name = namespace[namespace.length - 1],
		value = ast.value

	assert(!baseValue['__alias__' + name], ast, 'Repeat declaration')
	if (value.type == 'NESTED_ALIAS') {
		// store the nested alias, and then create a child-alias for each nested property
		// e.g. let foo = { width: 1 } allows you to reference foo.width as well as do e.g. style=foo
		baseValue['__alias__' + name] = value
		for (var i=0, content; content = value.content[i]; i++) {
			var newNamespace = util.copyArray(ast.namespace).concat(content.name),
				newAST = util.shallowCopy(ast, { type: 'ALIAS', value: content.value, namespace: newNamespace })

			_storeAlias(context, newAST)
		}
	} else {
		baseValue['__alias__' + name] = ast.value
	}
}
var _getAlias = function(context, ast, skipLast) {
	var referenceTable = context.referenceTable,
		value = referenceTable,
		namespace = ast.namespace,
		len = namespace.length - (skipLast ? 1 : 0)

	for (var i=0; i < len; i++) {
		value = value['__alias__' + namespace[i]]
		if (!value) {
			halt(ast, 'Undeclared alias "'+namespace[i]+'"' + (i == 0 ? '' : ' on "'+namespace.slice(0, i).join('.')+'"'))
		}
		if (value.type == 'ITEM') {
			return util.shallowCopy(ast, { type: 'ITEM_PROPERTY', item:value, property:namespace.slice(i+1) })
		}
	}

	return value
}

var resolve = function(context, aliasOrValue) {
	if (aliasOrValue.type != 'ALIAS') { return aliasOrValue }
	else { return resolve(context, _getAlias(context, aliasOrValue)) }
}

/**********************
 * If/Else statements *
 **********************/
function compileIfStatement(context, ast) {
	var left = resolve(context, ast.condition.left),
		right = resolve(context, ast.condition.right),
		ifContext = util.shallowCopy(context, { hookName: name('IF_HOOK') }),
		elseContext = util.shallowCopy(context, { hookName: name('ELSE_HOOK') }),
		isDynamic = { left:(left.type == 'ITEM_PROPERTY'), right:(right.type == 'ITEM_PROPERTY') }

	return code(
		'var {{ ifHookName }} = fun.name(),',
		'	{{ elseHookName }} = fun.name()',
		';(function(ifBranch, elseBranch) {',
		'	fun.hook({{ parentHookName }}, {{ ifHookName }})',
		'	fun.hook({{ parentHookName }}, {{ elseHookName }})',
		'	var ready = fun.block(evaluate, {fireOnce: false}), lastTime',
		'	{{ leftIsDynamic }} && fun.observe("BYTES", {{ leftID }}, {{ leftProperty }}, ready.addBlock())',
		'	{{ rightIsDynamic }} && fun.observe("BYTES", {{ rightID }}, {{ rightProperty }}, ready.addBlock())',
		'	ready.tryNow()',
		'	function evaluate() {',
		'		var thisTime = {{ leftValue }} {{ comparison }} {{ rightValue }}',
		'		if (lastTime !== undefined && thisTime == lastTime) { return }',
		'		fun.destroyHook(lastTime ? {{ ifHookName }} : {{ elseHookName }})',
		'		lastTime = thisTime',
		'		thisTime ? ifBranch() : elseBranch()',
		'	}',
		'})(',
		'	function() {',
		'		{{ ifCode }}',
		'	},',
		'	function() {',
		'		{{ elseCode }}',
		'	}',
		');',
		{
			parentHookName: context.hookName,
			ifHookName: ifContext.hookName,
			elseHookName: elseContext.hookName,
			leftIsDynamic: isDynamic.left,
			rightIsDynamic: isDynamic.right,
			leftValue: isDynamic.left ? 'fun.cachedValue({{ leftID }}, {{ leftProperty }})' : left.value,
			rightValue: isDynamic.right ? 'fun.cachedValue({{ rightID }}, {{ rightProperty }})' : right.value,
			comparison: ast.condition.comparison,
			leftID: isDynamic.left && q(left.item.id),
			rightID: isDynamic.right && q(right.item.id),
			leftProperty: isDynamic.left && q(left.property[0]),
			rightProperty: isDynamic.right && q(right.property[0]),
			ifCode: compile(ifContext, ast.ifBlock),
			elseCode: ast.elseBlock && compile(elseContext, ast.elseBlock)
		})
}

/*************
 * For loops *
 *************/
function compileForLoop(context, ast) {
	halt(ast, 'TODO compileForLoop not yet implemented')
}

/****************************************
 * Invocations (handlers and templates) *
 ****************************************/
function compileInvocation(context, ast) {
	halt(ast, 'TODO compileInvocation not yet implemented')
}

/*********************
 * Utility functions *
 *********************/
function name(readable) { return '_' + (readable || '') + '$' + (name._uniqueId++) }
name._uniqueId = 0

var emitReplaceRegex = /{{\s*(\w+)\s*}}/
function code(/* line1, line2, line3, ..., lineN, optionalValues */) {
	var argsLen = arguments.length,
		lastArg = arguments[argsLen - 1],
		injectObj = (typeof lastArg == 'string' ? null : lastArg),
		snippets = Array.prototype.slice.call(arguments, 0, injectObj ? argsLen - 1 : argsLen),
		code = '\n' + snippets.join('\n'),
		match

	while (match = code.match(emitReplaceRegex)) {
		var wholeMatch = match[0],
			nameMatch = match[1],
			value = injectObj[nameMatch]
		code = code.replace(wholeMatch, typeof value == 'undefined' ? 'MISSING INJECT VALUE' : value)
	}
	return code
}

var CompileError = function(file, ast, msg) {
	this.name = "CompileError"
	this.message = ['on line', ast.line + ',', 'column', ast.column, 'of', '"'+file+'":', msg].join(' ')
}
CompileError.prototype = Error.prototype

var assert = function(ok, ast, msg) { if (!ok) halt(ast, msg) }
var halt = function(ast, msg) {
	sys.puts(util.grabLine(ast.file, ast.line, ast.column, ast.span))
	throw new CompileError(ast.file, ast, msg)
}<commit_msg>Rename compileDeclaration to handleDeclaration since it doesn't actually compile any code but rather just stores an alias<commit_after>var fs = require('fs'),
	sys = require('sys'),
	path = require('path'),
	util = require('./util'),
	bind = util.bind,
	map = util.map,
	repeat = util.repeat,
	boxComment = util.boxComment,
	q = util.q,
	tokenizer = require('./tokenizer'),
	parser = require('./parser'),
	compiler = exports,
	gModules = {}

exports.compile = util.intercept('CompileError', function (ast, context) {

	context = context || { hookName: name('ROOT_HOOK'), referenceTable: {} } // root context

	return code(
		';(function funApp() {',
		'	var {{ hookName }} = fun.name("rootHook")',
		'	fun.setHook({{ hookName }}, document.body)',
		'	{{ code }}',
		'	{{ modules }}',
		'})(); // let\'s kick it',
		{
			hookName: context.hookName,
			code: compile(context, ast),
			modules: map(gModules, function(module, name) {
				return boxComment('Module: ' + name) + '\n' + module.jsCode }).join('\n\n\n')
		})
})

/************************
 * Top level statements *
 ************************/
function compile(context, ast) {
	assert(context && context.hookName && context.referenceTable, ast, "compile called with invalid context")
	if (ast instanceof Array) {
		return map(ast, bind(this, compileStatement, context)).join('\n') + '\n'
	} else {
		return compileStatement(context, ast) + '\n'
	}
}

function compileStatement(context, ast) {
	switch (ast.type) {
		case 'STATIC_VALUE':    return compileStaticValue(context, ast)
		case 'ALIAS':           return compileStatement(context, resolve(context, ast))
		case 'ITEM_PROPERTY':   return compileItemProperty(context, ast)
		case 'XML':             return compileXML(context, ast)
		case 'DECLARATION':     return handleDeclaration(context, ast)
		case 'IF_STATEMENT':    return compileIfStatement(context, ast)
		case 'FOR_LOOP':        return compileForLoop(context, ast)
		case 'INVOCATION':      return compileInvocation(context, ast)
		case 'IMPORT_MODULE':   return compileModuleImport(context, ast)
		case 'IMPORT_FILE':     return compileFileImport(context, ast)
		default:                halt(ast, 'Unknown AST type ' + ast.type)
	}
}

/*****************
 * Static values *
 *****************/
function compileStaticValue(context, ast) {
	return code(
		'fun.hook({{ parentHook }}, fun.name("inlineString")).innerHTML = {{ value }}',
		{
			parentHook: context.hookName,
			value: JSON.stringify(ast.value)
		})
}

/************************
 * Item Property values *
 ************************/
function compileItemProperty(context, ast) {
	assert(ast.property.length > 0, ast, 'Missing property on item reference. "'+ast.namespace[0]+'" should probably be something like "'+ast.namespace[0]+'.foo"')
	assert(ast.property.length == 1, ast, 'TODO: Handle nested property references')
	var hookName = name('ITEM_PROPERTY_HOOK')
	return code(
		'fun.hook({{ parentHook }}, {{ hookName }})',
		'fun.observe({{ type }}, {{ id }}, {{ property }}, function(mutation, value) {',
		'	fun.getHook({{ hookName }}).innerHTML = value',
		'})',
		{
			parentHook: context.hookName,
			hookName: q(hookName),
			id: q(ast.item.id),
			property: q(ast.property[0]),
			type: q('BYTES')
		})
}

/*******
 * XML *
 *******/
function compileXML(context, ast) {
	var hookName = name('XML_HOOK'),
		newContext = util.shallowCopy(context, { hookName:hookName })

	var attributes = _handleXMLAttributes(context, ast, hookName)
	return code(
		'var {{ hookName }} = fun.name()',
		'fun.hook({{ parentHook }}, {{ hookName }}, {{ tagName }}, {{ staticAttributes }})',
		'{{ dynamicAttributesCode }}',
		'{{ childCode }}',
		{
			parentHook: context.hookName,
			hookName: hookName,
			tagName: q(ast.tag),
			staticAttributes: q(attributes.staticAttributes),
			dynamicAttributesCode: attributes.dynamicCode,
			childCode: ast.block ? compile(newContext, ast.block) : ''
		})
}

function _handleXMLAttributes(context, ast, hookName) {
	var staticAttributes = {},
		dynamicCode = []
	for (var i=0, attribute; attribute = ast.attributes[i]; i++) {
		assert(attribute.namespace.length == 1, ast, 'TODO Handle dot notation XML attribute namespace (for e.g. style.width=100)')
		var name = attribute.namespace[0],
			value = resolve(context, attribute.value),
			match
		if (name == 'style') {
			assert(value.type == 'NESTED_ALIAS', ast, 'You can only assign the style attribute to a JSON object literal, e.g.
')
			_handleStyleAttribute(staticAttributes, dynamicCode, context, ast, hookName, value.content)
		} else if (match = name.match(/^on(\w+)$/)) {
			halt('TODO Handle on* xml attributes')
		} else if (value.type == 'STATIC_VALUE') {
			staticAttributes[name] = value.value
		} else {
			assert(value.type != 'NESTED_ALIAS', ast, 'Does not make sense to assign a JSON object literal to other attribtues than "style" (tried to assign to "'+name+'")')
			_handleDynamicAttribute(dynamicCode, context, ast, hookName, name, value)
		}
	}
	return { staticAttributes: staticAttributes, dynamicCode: dynamicCode.join('\n') }
}

// add static attributes to staticAttrs object
function _handleStyleAttribute(staticAttrs, dynamicCode, context, ast, hookName, styles) {
	var styleAttribute = staticAttrs['style'] = {}
	for (var i=0, style; style = styles[i]; i++) {
		var value = resolve(context, style.value)
		if (value.type == 'STATIC_VALUE') {
			styleAttribute[style.name] = value.value
		} else {
			_handleDynamicAttribute(dynamicCode, context, ast, hookName, 'style.' + style.name, value)
		}
	}
}

// add code for dynamic properties to dynamicCode
function _handleDynamicAttribute(dynamicCode, context, ast, hookName, attrName, value) {
	assert(value.property.length == 1, ast, 'TODO: Handle nested item property references')
	dynamicCode.push(code(
		'fun.observe({{ type }}, {{ id }}, {{ property }}, function(mutation, value) {',
		'	fun.attr({{ hookName }}, {{ attr }}, value)',
		'})',
		{
			type: q('BYTES'),
			id: q(value.item.id),
			property: q(value.property[0]),
			attr: q(attrName),
			hookName: hookName
		}))
}

/*************************
 * Module & File Imports *
 *************************/
function compileModuleImport(context, ast) {
	if (gModules[ast.name]) { return }
	var module = gModules[ast.name] = {
		name: ast.name,
		path: __dirname + '/modules/' + ast.name + '/'
	}
	assert(fs.statSync(module.path).isDirectory(), ast, 'Could not find the module at ' + module.path)
	// TODO Read a package/manifest.json file in the module directory, describing name/version/which files to load, etc
	if (fs.statSync(module.path + 'lib.fun').isFile()) {
		var result = _importFile(module.path + 'lib.fun', context)
	}
	if (path.existsSync(module.path + 'lib.js')) {
		module.jsCode = fs.readFileSync(module.path + 'lib.js')
	} else {
		module.jsCode = '// No JS code for ' + module.name
	}
	return result
}

function compileFileImport(context, ast) {
	var filePath = __dirname + '/' + ast.path + '.fun'
	assert(path.existsSync(filePath), ast, 'Could not find file for import: "'+filePath+'"')
	return _importFile(filePath, context)
}

function _importFile(path, context) {
	var tokens = tokenizer.tokenize(path)
	var newAST = parser.parse(tokens)
	return compile(context, newAST)
}

/**********************
 * Alias Declarations *
 **********************/

function handleDeclaration(context, ast) {
	assert(ast.type == 'DECLARATION', ast)
	_storeAlias(context, ast)
	return ''
}

var _storeAlias = function(context, ast) {
	var baseValue = _getAlias(context, ast, true),
		namespace = ast.namespace,
		name = namespace[namespace.length - 1],
		value = ast.value

	assert(!baseValue['__alias__' + name], ast, 'Repeat declaration')
	if (value.type == 'NESTED_ALIAS') {
		// store the nested alias, and then create a child-alias for each nested property
		// e.g. let foo = { width: 1 } allows you to reference foo.width as well as do e.g. style=foo
		baseValue['__alias__' + name] = value
		for (var i=0, content; content = value.content[i]; i++) {
			var newNamespace = util.copyArray(ast.namespace).concat(content.name),
				newAST = util.shallowCopy(ast, { type: 'ALIAS', value: content.value, namespace: newNamespace })

			_storeAlias(context, newAST)
		}
	} else {
		baseValue['__alias__' + name] = ast.value
	}
}
var _getAlias = function(context, ast, skipLast) {
	var referenceTable = context.referenceTable,
		value = referenceTable,
		namespace = ast.namespace,
		len = namespace.length - (skipLast ? 1 : 0)

	for (var i=0; i < len; i++) {
		value = value['__alias__' + namespace[i]]
		if (!value) {
			halt(ast, 'Undeclared alias "'+namespace[i]+'"' + (i == 0 ? '' : ' on "'+namespace.slice(0, i).join('.')+'"'))
		}
		if (value.type == 'ITEM') {
			return util.shallowCopy(ast, { type: 'ITEM_PROPERTY', item:value, property:namespace.slice(i+1) })
		}
	}

	return value
}

var resolve = function(context, aliasOrValue) {
	if (aliasOrValue.type != 'ALIAS') { return aliasOrValue }
	else { return resolve(context, _getAlias(context, aliasOrValue)) }
}

/**********************
 * If/Else statements *
 **********************/
function compileIfStatement(context, ast) {
	var left = resolve(context, ast.condition.left),
		right = resolve(context, ast.condition.right),
		ifContext = util.shallowCopy(context, { hookName: name('IF_HOOK') }),
		elseContext = util.shallowCopy(context, { hookName: name('ELSE_HOOK') }),
		isDynamic = { left:(left.type == 'ITEM_PROPERTY'), right:(right.type == 'ITEM_PROPERTY') }

	return code(
		'var {{ ifHookName }} = fun.name(),',
		'	{{ elseHookName }} = fun.name()',
		';(function(ifBranch, elseBranch) {',
		'	fun.hook({{ parentHookName }}, {{ ifHookName }})',
		'	fun.hook({{ parentHookName }}, {{ elseHookName }})',
		'	var ready = fun.block(evaluate, {fireOnce: false}), lastTime',
		'	{{ leftIsDynamic }} && fun.observe("BYTES", {{ leftID }}, {{ leftProperty }}, ready.addBlock())',
		'	{{ rightIsDynamic }} && fun.observe("BYTES", {{ rightID }}, {{ rightProperty }}, ready.addBlock())',
		'	ready.tryNow()',
		'	function evaluate() {',
		'		var thisTime = {{ leftValue }} {{ comparison }} {{ rightValue }}',
		'		if (lastTime !== undefined && thisTime == lastTime) { return }',
		'		fun.destroyHook(lastTime ? {{ ifHookName }} : {{ elseHookName }})',
		'		lastTime = thisTime',
		'		thisTime ? ifBranch() : elseBranch()',
		'	}',
		'})(',
		'	function() {',
		'		{{ ifCode }}',
		'	},',
		'	function() {',
		'		{{ elseCode }}',
		'	}',
		');',
		{
			parentHookName: context.hookName,
			ifHookName: ifContext.hookName,
			elseHookName: elseContext.hookName,
			leftIsDynamic: isDynamic.left,
			rightIsDynamic: isDynamic.right,
			leftValue: isDynamic.left ? 'fun.cachedValue({{ leftID }}, {{ leftProperty }})' : left.value,
			rightValue: isDynamic.right ? 'fun.cachedValue({{ rightID }}, {{ rightProperty }})' : right.value,
			comparison: ast.condition.comparison,
			leftID: isDynamic.left && q(left.item.id),
			rightID: isDynamic.right && q(right.item.id),
			leftProperty: isDynamic.left && q(left.property[0]),
			rightProperty: isDynamic.right && q(right.property[0]),
			ifCode: compile(ifContext, ast.ifBlock),
			elseCode: ast.elseBlock && compile(elseContext, ast.elseBlock)
		})
}

/*************
 * For loops *
 *************/
function compileForLoop(context, ast) {
	halt(ast, 'TODO compileForLoop not yet implemented')
}

/****************************************
 * Invocations (handlers and templates) *
 ****************************************/
function compileInvocation(context, ast) {
	halt(ast, 'TODO compileInvocation not yet implemented')
}

/*********************
 * Utility functions *
 *********************/
function name(readable) { return '_' + (readable || '') + '$' + (name._uniqueId++) }
name._uniqueId = 0

var emitReplaceRegex = /{{\s*(\w+)\s*}}/
function code(/* line1, line2, line3, ..., lineN, optionalValues */) {
	var argsLen = arguments.length,
		lastArg = arguments[argsLen - 1],
		injectObj = (typeof lastArg == 'string' ? null : lastArg),
		snippets = Array.prototype.slice.call(arguments, 0, injectObj ? argsLen - 1 : argsLen),
		code = '\n' + snippets.join('\n'),
		match

	while (match = code.match(emitReplaceRegex)) {
		var wholeMatch = match[0],
			nameMatch = match[1],
			value = injectObj[nameMatch]
		code = code.replace(wholeMatch, typeof value == 'undefined' ? 'MISSING INJECT VALUE' : value)
	}
	return code
}

var CompileError = function(file, ast, msg) {
	this.name = "CompileError"
	this.message = ['on line', ast.line + ',', 'column', ast.column, 'of', '"'+file+'":', msg].join(' ')
}
CompileError.prototype = Error.prototype

var assert = function(ok, ast, msg) { if (!ok) halt(ast, msg) }
var halt = function(ast, msg) {
	sys.puts(util.grabLine(ast.file, ast.line, ast.column, ast.span))
	throw new CompileError(ast.file, ast, msg)
}<|endoftext|>