<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

import {toArray} from "../utils";
import {h} from "inferno-hyperscript";
import {VirtualDOMView} from "./VirtualDOMView";
import Jed from "jed";

/*
 * The Timeline view shows a table where columns represent frames
 * and rows represent layers.
 * The user can choose to manipulate only a subset of the layers of the document.
 * The other layers are grouped into a "Default" row.
 *
 * The Timeline view allows the user to:
 *  - choose editable layers from the document
 *  - show/hide layers
 *  - create/delete/reorder frames
 *  - add/remove layers and frames to/from the selection
 */
export const Timeline = Object.create(VirtualDOMView);

/*
 * Initialize a Timeline view.
 *
 * The view is rendered and event handlers are set up.
 *
 * Parameters:
 *  - presentation: a presentation object
 *  - selection: a selection object
 *
 * Returns:
 *  - The current view
 */
Timeline.init = function (container, presentation, selection, controller, locale) {
    VirtualDOMView.init.call(this, container, controller);

    this.presentation = presentation;
    this.selection = selection;
    this.gettext = s => locale.gettext(s);

    this.editableLayers = [];
    this.defaultLayers = [];

    controller.addListener("ready", () => this.onReady());

    return this;
};

Timeline.onReady = function () {
    this.defaultLayers = [];

    this.presentation.layers.forEach(layer => {
        if (this.editableLayers.indexOf(layer) < 0) {
            this.defaultLayers.push(layer);
        }
    });
};

Timeline.toStorable = function () {
    return {
        editableLayers: this.editableLayers.map(layer => layer.groupId)
    };
};

Timeline.fromStorable = function (storable) {
    this.editableLayers = [];

    if (storable.hasOwnProperty("editableLayers")) {
        storable.editableLayers.forEach(groupId => {
            const layer = this.presentation.getLayerWithId(groupId);
            if (layer && this.editableLayers.indexOf(layer) < 0) {
                this.editableLayers.push(layer);
            }
        });
    }
    return this;
};

/*
 * Action: Add a layer to the current view.
 *
 * This method is called as a result of a user action
 * in the current view.
 *
 * The layer at the given index is added to the current view
 * and to the selection.
 * The current view is updated.
 *
 * Parameters:
 *  - layerIndex: The index of a layer in the presentation
 */
Timeline.addLayer = function (layerIndex) {
    const layer = this.presentation.layers[layerIndex];
    if (this.editableLayers.indexOf(layer) < 0) {
        this.editableLayers.push(layer);
    }

    const layerIndexInDefaults = this.defaultLayers.indexOf(layer);
    this.defaultLayers.splice(layerIndexInDefaults, 1);

    this.controller.addLayerToSelection(layer);

    // Force a repaint even if the controller
    // did not modify the selection
    this.repaint();
};

/*
 * Action: Remove a layer from the current view.
 *
 * This method is called as a result of a user action
 * in the current view.
 *
 * The layer at the given index is removed from the current view
 * and from the selection.
 * The current view is updated.
 *
 * Parameters:
 *  - layerIndex: The index of a layer in the presentation
 */
Timeline.removeLayer = function (layerIndex) {
    const layer = this.presentation.layers[layerIndex];

    const layerIndexInEditable = this.editableLayers.indexOf(layer);
    this.editableLayers.splice(layerIndexInEditable, 1);

    if (this.defaultLayersAreSelected) {
        this.controller.addLayerToSelection(layer);
    }
    else if (this.selection.selectedLayers.length > 1) {
        this.controller.removeLayerFromSelection(layer);
    }
    else {
        this.controller.selectLayers(this.defaultLayers);
    }

    this.defaultLayers.push(layer);

    // Force a repaint even if the controller
    // did not modify the selection
    this.repaint();
};

Timeline.toggleLayerVisibility = function (layerIndex, evt) {
    this.controller.updateLayerVisibility(this.getLayersAtIndex(layerIndex));
    evt.stopPropagation();
};

Timeline.getLayersAtIndex = function (layerIndex) {
    return layerIndex >= 0 ?
        [this.presentation.layers[layerIndex]] :
        this.defaultLayers;
};

/*
 * Check whether "default" layers are selected.
 *
 * Returns:
 *  - true if all "default" layers are selected, else false
 */
Object.defineProperty(Timeline, "defaultLayersAreSelected", {
    get() {
        return this.defaultLayers.every(layer => this.selection.selectedLayers.indexOf(layer) >= 0);
    }
});

Timeline.updateFrameSelection = function (frameIndex, evt) {
    this.controller.updateFrameSelection(evt.ctrlKey, evt.shiftKey, frameIndex);
    evt.stopPropagation();
};

Timeline.updateLayerSelection = function (layerIndex, evt) {
    this.controller.updateLayerSelection(evt.ctrlKey, evt.shiftKey, this.getLayersAtIndex(layerIndex));
    evt.stopPropagation();
};

Timeline.updateLayerAndFrameSelection = function (layerIndex, frameIndex, evt) {
    this.controller.updateLayerAndFrameSelection(evt.ctrlKey, evt.shiftKey, this.getLayersAtIndex(layerIndex), frameIndex);
    evt.stopPropagation();
};

Timeline.repaint = function () {
    VirtualDOMView.repaint.call(this);

    const topLeft = this.container.querySelector(".timeline-top-left");
    const topRight = this.container.querySelector(".timeline-top-right");
    const bottomLeft = this.container.querySelector(".timeline-bottom-left");
    const bottomRight = this.container.querySelector(".timeline-bottom-right");

    const topLeftTable = topLeft.querySelector("table");
    const topRightTable = topRight.querySelector("table");
    const bottomLeftTable = bottomLeft.querySelector("table");

    const leftWidth = Math.max(topLeftTable.clientWidth, bottomLeftTable.clientWidth);
    const rightWidth = this.container.clientWidth - leftWidth;
    const topHeight = Math.max(topLeftTable.clientHeight, topRightTable.clientHeight);
    const bottomHeight = this.container.clientHeight - topHeight;

    // Fit the width of the left tables,
    // allocate remaining width to the right tables
    topLeft.style.width = bottomLeft.style.width =
    topLeftTable.style.width = bottomLeftTable.style.width = leftWidth + "px";
    topRight.style.width = bottomRight.style.width = rightWidth + "px";

    // Fit the height of the top tables,
    // allocate remaining width to the bottom tables
    topLeft.style.height = topRight.style.height = topHeight + "px";
    bottomLeft.style.height = bottomRight.style.height = bottomHeight + "px";

    // Corresponding rows in left and right tables must have the same height
    const leftRows  = toArray(topLeft.querySelectorAll("tr")).concat(toArray(bottomLeft.querySelectorAll("tr")));
    const rightRows = toArray(topRight.querySelectorAll("tr")).concat(toArray(bottomRight.querySelectorAll("tr")));
    leftRows.forEach((leftRow, rowIndex) => {
        const rightRow = rightRows[rowIndex];
        const maxHeight = Math.max(leftRow.clientHeight, rightRow.clientHeight);
        leftRow.style.height = rightRow.style.height = maxHeight + "px";
    });
};

Object.defineProperty(Timeline, "hasDefaultLayer", {
    get() {
        return this.defaultLayers.length > 1 ||
               this.defaultLayers.length > 0 && this.defaultLayers[0].svgNodes.length;
    }
});

Object.defineProperty(Timeline, "refLayerInDefault", {
    get() {
        for (let i = 0; i < this.defaultLayers.length; i ++) {
            if (this.defaultLayers[i].svgNodes.length) {
                return this.defaultLayers[i];
            }
        }
        return this.defaultLayers[0];
    }
});

Timeline.render = function () {
    const _ = this.gettext;

    const defaultLayersAreVisible = this.defaultLayers.some(layer => layer.isVisible);

    const c = this.controller;
    let even = true;
    function updateEven(frame, layer) {
        if (frame.index === 0) {
            even = true;
        }
        else if (!frame.layerProperties[layer.index].link) {
            even = !even;
        }
        return even;
    }

    return h("div", [
        h("div.timeline-top-left", [
            h("table.timeline", [
                h("tr", [
                    h("th", [
                        h("button", {
                            title: _("Delete the selected frames"),
                            disabled: this.selection.selectedFrames.length ? undefined : "disabled",
                            onclick() { c.deleteFrames(); }
                        }, h("i.fas.fa-trash"))
                    ]),
                    h("th", [
                        h("button", {
                            title: _("Create a new frame"),
                            onclick() { c.addFrame(); }
                        }, h("i.fas.fa-plus"))
                    ]),
                ]),
                h("tr", [
                    h("th", {colspan: 2},
                        h("select", {
                            onchange: evt => {
                                const value = evt.target.value;
                                evt.target.value = "__add__";
                                this.addLayer(value);
                            }
                        }, [
                            h("option", {value: "__add__", selected: "selected"}, _("Add layer")),
                            this.presentation.layers.slice().reverse()
                                .filter(layer => !layer.auto && this.defaultLayers.indexOf(layer) >= 0)
                                .map(layer => h("option", {value: layer.index}, layer.label))
                        ])
                    )
                ])
            ])
        ]),
        h("div.timeline-bottom-left", [
            h("table.timeline", [
                this.hasDefaultLayer ? h("tr", [
                    h("th.layer-icons", [
                        defaultLayersAreVisible ?
                            h("i.visibility.far.fa-eye", {
                                title: _("This layer is visible. Click to hide it."),
                                onclick: evt => this.toggleLayerVisibility(-1, evt)
                            }) :
                            h("i.visibility.far.fa-eye-slash", {
                                title: _("This layer is hidden. Click to show it."),
                                onclick: evt => this.toggleLayerVisibility(-1, evt)
                            }),
                        h("i.remove.fas.fa-times", {style: {visibility: "hidden"}})
                    ]),
                    h("th", {
                        className: "layer-label" + (this.defaultLayersAreSelected ? " selected" : ""),
                        onclick: evt => this.updateLayerSelection(-1, evt)
                    }, _("Default"))
                ]) : null,
                this.presentation.layers.slice().reverse()
                    .filter(layer => this.editableLayers.indexOf(layer) >= 0)
                    .map(layer => h("tr", [
                            h("th.layer-icons", [
                                layer.isVisible ?
                                    h("i.visibility.far.fa-eye", {
                                        title: _("This layer is visible. Click to hide it."),
                                        onclick: evt => this.toggleLayerVisibility(layer.index, evt)
                                    }) :
                                    h("i.visibility.far.fa-eye-slash", {
                                        title: _("This layer is hidden. Click to show it."),
                                        onclick: evt => this.toggleLayerVisibility(layer.index, evt)
                                    }),
                                h("i.remove.fas.fa-times", {
                                    title: _("Remove this layer"),
                                    onclick: () => this.removeLayer(layer.index)
                                })
                            ]),
                            h("th", {
                                className: "layer-label" + (this.selection.selectedLayers.indexOf(layer) >= 0 ? " selected" : ""),
                                onclick: evt => this.updateLayerSelection(layer.index, evt)
                            }, layer.label)
                        ])
                    ),
                h("tr", {style: {visibility: "collapse"}}, [
                    h("th.layer-icons"),
                    h("th.layer-label", _("Default"))
                ])
            ])
        ]),
        h("div.timeline-top-right", [
            h("table.timeline", [
                h("tr", this.presentation.frames.map((frame, frameIndex)  => h("th", {
                        className: "frame-index" +
                            (this.selection.selectedFrames.indexOf(frame) >= 0 ? " selected" : "") +
                            (frame === this.selection.currentFrame ? " current" : ""),
                        onclick: evt => this.updateFrameSelection(frameIndex, evt)
                    }, [
                        h("i.insert-before.fas.fa-arrow-circle-down", {
                            title: Jed.sprintf(_("Insert selection before frame %d"), frameIndex + 1),
                            onclick(evt) {
                                c.moveFrames(frameIndex);
                                evt.stopPropagation();
                            }
                        }),
                        h("i.insert-after.fas.fa-arrow-circle-down", {
                            title: Jed.sprintf(_("Insert selection after frame %d"), frameIndex + 1),
                            onclick(evt) {
                                c.moveFrames(frameIndex + 1);
                                evt.stopPropagation();
                            }
                        }),
                        (frameIndex + 1).toString()
                    ])
                )),
                h("tr",
                  this.presentation.frames.map((frame, frameIndex) => h("th", {
                            title: frame.title,
                            className: "frame-title" +
                                (this.selection.selectedFrames.indexOf(frame) >= 0 ? " selected" : "") +
                                (frame === this.selection.currentFrame ? " current" : ""),
                            onclick: evt => this.updateFrameSelection(frameIndex, evt)
                        }, frame.title)
                    )
                 )
            ])
        ]),
        h("div.timeline-bottom-right", {
            onscroll: evt => {
                this.container.querySelector(".timeline-top-right").scrollLeft = evt.target.scrollLeft;
                this.container.querySelector(".timeline-bottom-left").scrollTop = evt.target.scrollTop;
            }
        }, h("table.timeline", [
                this.hasDefaultLayer ? h("tr",
                    this.presentation.frames.map((frame, frameIndex) => h("td", {
                        className:
                            (this.defaultLayersAreSelected && this.selection.selectedFrames.indexOf(frame) >= 0 ? "selected" : "") +
                            (frame === this.selection.currentFrame ? " current" : "") +
                            (frameIndex > 0 && frame.layerProperties[this.defaultLayers[0].index].link ? " link" : "") +
                            (updateEven(frame, this.defaultLayers[0]) ? " even" : " odd"),
                        onclick: evt => this.updateLayerAndFrameSelection(-1, frameIndex, evt)
                    }))
                ) : null,
                this.presentation.layers.slice().reverse()
                    .filter(layer => this.editableLayers.indexOf(layer) >= 0)
                    .map(layer => h("tr",
                        this.presentation.frames.map((frame, frameIndex) => h("td", {
                            className:
                                (this.selection.selectedLayers.indexOf(layer) >= 0 && this.selection.selectedFrames.indexOf(frame) >= 0 ? "selected" : "") +
                                (frame === this.selection.currentFrame ? " current" : "") +
                                (frameIndex > 0 && frame.layerProperties[layer.index].link ? " link" : "") +
                                (updateEven(frame, layer) ? " even" : " odd"),
                            onclick: evt => this.updateLayerAndFrameSelection(layer.index, frameIndex, evt)
                        })
                    ))),
                h("tr.collapse",
                    this.presentation.frames.map(frame => h("td", frame.title))
                )
            ])
        )
    ]);
};
<commit_msg>Fixes #453 - Add an option to add all layers to a document<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

import {toArray} from "../utils";
import {h} from "inferno-hyperscript";
import {VirtualDOMView} from "./VirtualDOMView";
import Jed from "jed";

/*
 * The Timeline view shows a table where columns represent frames
 * and rows represent layers.
 * The user can choose to manipulate only a subset of the layers of the document.
 * The other layers are grouped into a "Default" row.
 *
 * The Timeline view allows the user to:
 *  - choose editable layers from the document
 *  - show/hide layers
 *  - create/delete/reorder frames
 *  - add/remove layers and frames to/from the selection
 */
export const Timeline = Object.create(VirtualDOMView);

/*
 * Initialize a Timeline view.
 *
 * The view is rendered and event handlers are set up.
 *
 * Parameters:
 *  - presentation: a presentation object
 *  - selection: a selection object
 *
 * Returns:
 *  - The current view
 */
Timeline.init = function (container, presentation, selection, controller, locale) {
    VirtualDOMView.init.call(this, container, controller);

    this.presentation = presentation;
    this.selection = selection;
    this.gettext = s => locale.gettext(s);

    this.editableLayers = [];
    this.defaultLayers = [];

    controller.addListener("ready", () => this.onReady());

    return this;
};

Timeline.onReady = function () {
    this.defaultLayers = [];

    this.presentation.layers.forEach(layer => {
        if (this.editableLayers.indexOf(layer) < 0) {
            this.defaultLayers.push(layer);
        }
    });
};

Timeline.toStorable = function () {
    return {
        editableLayers: this.editableLayers.map(layer => layer.groupId)
    };
};

Timeline.fromStorable = function (storable) {
    this.editableLayers = [];

    if (storable.hasOwnProperty("editableLayers")) {
        storable.editableLayers.forEach(groupId => {
            const layer = this.presentation.getLayerWithId(groupId);
            if (layer && this.editableLayers.indexOf(layer) < 0) {
                this.editableLayers.push(layer);
            }
        });
    }
    return this;
};

/*
 * Action: Add a layer to the current view.
 *
 * This method is called as a result of a user action
 * in the current view.
 *
 * The layer at the given index is added to the current view
 * and to the selection.
 * The current view is updated.
 *
 * Parameters:
 *  - layerIndex: The index of a layer in the presentation
 */
Timeline.addLayer = function (layerIndex) {
    const layer = this.presentation.layers[layerIndex];
    if (this.editableLayers.indexOf(layer) < 0) {
        this.editableLayers.push(layer);
    }

    const layerIndexInDefaults = this.defaultLayers.indexOf(layer);
    if (layerIndexInDefaults >= 0) {
        this.defaultLayers.splice(layerIndexInDefaults, 1);
    }

    this.controller.addLayerToSelection(layer);

    // Force a repaint even if the controller
    // did not modify the selection
    this.repaint();
};

/*
 * Action: Add all layers to the current view.
 *
 * This method is called as a result of a user action
 * in the current view.
 *
 * All layers are added to the current view
 * and to the selection.
 * The current view is updated.
 */
Timeline.addAllLayers = function () {
    this.defaultLayers.slice().forEach(layer => {
        if (layer.auto) {
            return;
        }

        this.editableLayers.push(layer);

        const layerIndexInDefaults = this.defaultLayers.indexOf(layer);
        this.defaultLayers.splice(layerIndexInDefaults, 1);

        this.controller.addLayerToSelection(layer);
    });

    // Force a repaint even if the controller
    // did not modify the selection
    this.repaint();
};

/*
 * Action: Remove a layer from the current view.
 *
 * This method is called as a result of a user action
 * in the current view.
 *
 * The layer at the given index is removed from the current view
 * and from the selection.
 * The current view is updated.
 *
 * Parameters:
 *  - layerIndex: The index of a layer in the presentation
 */
Timeline.removeLayer = function (layerIndex) {
    const layer = this.presentation.layers[layerIndex];

    const layerIndexInEditable = this.editableLayers.indexOf(layer);
    this.editableLayers.splice(layerIndexInEditable, 1);

    if (this.defaultLayersAreSelected) {
        this.controller.addLayerToSelection(layer);
    }
    else if (this.selection.selectedLayers.length > 1) {
        this.controller.removeLayerFromSelection(layer);
    }
    else {
        this.controller.selectLayers(this.defaultLayers);
    }

    this.defaultLayers.push(layer);

    // Force a repaint even if the controller
    // did not modify the selection
    this.repaint();
};

Timeline.toggleLayerVisibility = function (layerIndex, evt) {
    this.controller.updateLayerVisibility(this.getLayersAtIndex(layerIndex));
    evt.stopPropagation();
};

Timeline.getLayersAtIndex = function (layerIndex) {
    return layerIndex >= 0 ?
        [this.presentation.layers[layerIndex]] :
        this.defaultLayers;
};

/*
 * Check whether "default" layers are selected.
 *
 * Returns:
 *  - true if all "default" layers are selected, else false
 */
Object.defineProperty(Timeline, "defaultLayersAreSelected", {
    get() {
        return this.defaultLayers.every(layer => this.selection.selectedLayers.indexOf(layer) >= 0);
    }
});

Timeline.updateFrameSelection = function (frameIndex, evt) {
    this.controller.updateFrameSelection(evt.ctrlKey, evt.shiftKey, frameIndex);
    evt.stopPropagation();
};

Timeline.updateLayerSelection = function (layerIndex, evt) {
    this.controller.updateLayerSelection(evt.ctrlKey, evt.shiftKey, this.getLayersAtIndex(layerIndex));
    evt.stopPropagation();
};

Timeline.updateLayerAndFrameSelection = function (layerIndex, frameIndex, evt) {
    this.controller.updateLayerAndFrameSelection(evt.ctrlKey, evt.shiftKey, this.getLayersAtIndex(layerIndex), frameIndex);
    evt.stopPropagation();
};

Timeline.repaint = function () {
    VirtualDOMView.repaint.call(this);

    const topLeft = this.container.querySelector(".timeline-top-left");
    const topRight = this.container.querySelector(".timeline-top-right");
    const bottomLeft = this.container.querySelector(".timeline-bottom-left");
    const bottomRight = this.container.querySelector(".timeline-bottom-right");

    const topLeftTable = topLeft.querySelector("table");
    const topRightTable = topRight.querySelector("table");
    const bottomLeftTable = bottomLeft.querySelector("table");

    const leftWidth = Math.max(topLeftTable.clientWidth, bottomLeftTable.clientWidth);
    const rightWidth = this.container.clientWidth - leftWidth;
    const topHeight = Math.max(topLeftTable.clientHeight, topRightTable.clientHeight);
    const bottomHeight = this.container.clientHeight - topHeight;

    // Fit the width of the left tables,
    // allocate remaining width to the right tables
    topLeft.style.width = bottomLeft.style.width =
    topLeftTable.style.width = bottomLeftTable.style.width = leftWidth + "px";
    topRight.style.width = bottomRight.style.width = rightWidth + "px";

    // Fit the height of the top tables,
    // allocate remaining width to the bottom tables
    topLeft.style.height = topRight.style.height = topHeight + "px";
    bottomLeft.style.height = bottomRight.style.height = bottomHeight + "px";

    // Corresponding rows in left and right tables must have the same height
    const leftRows  = toArray(topLeft.querySelectorAll("tr")).concat(toArray(bottomLeft.querySelectorAll("tr")));
    const rightRows = toArray(topRight.querySelectorAll("tr")).concat(toArray(bottomRight.querySelectorAll("tr")));
    leftRows.forEach((leftRow, rowIndex) => {
        const rightRow = rightRows[rowIndex];
        const maxHeight = Math.max(leftRow.clientHeight, rightRow.clientHeight);
        leftRow.style.height = rightRow.style.height = maxHeight + "px";
    });
};

Object.defineProperty(Timeline, "hasDefaultLayer", {
    get() {
        return this.defaultLayers.length > 1 ||
               this.defaultLayers.length > 0 && this.defaultLayers[0].svgNodes.length;
    }
});

Object.defineProperty(Timeline, "refLayerInDefault", {
    get() {
        for (let i = 0; i < this.defaultLayers.length; i ++) {
            if (this.defaultLayers[i].svgNodes.length) {
                return this.defaultLayers[i];
            }
        }
        return this.defaultLayers[0];
    }
});

Timeline.render = function () {
    const _ = this.gettext;

    const defaultLayersAreVisible = this.defaultLayers.some(layer => layer.isVisible);

    const c = this.controller;
    let even = true;
    function updateEven(frame, layer) {
        if (frame.index === 0) {
            even = true;
        }
        else if (!frame.layerProperties[layer.index].link) {
            even = !even;
        }
        return even;
    }

    return h("div", [
        h("div.timeline-top-left", [
            h("table.timeline", [
                h("tr", [
                    h("th", [
                        h("button", {
                            title: _("Delete the selected frames"),
                            disabled: this.selection.selectedFrames.length ? undefined : "disabled",
                            onclick() { c.deleteFrames(); }
                        }, h("i.fas.fa-trash"))
                    ]),
                    h("th", [
                        h("button", {
                            title: _("Create a new frame"),
                            onclick() { c.addFrame(); }
                        }, h("i.fas.fa-plus"))
                    ]),
                ]),
                h("tr", [
                    h("th", {colspan: 2},
                        h("select", {
                            onchange: evt => {
                                const value = evt.target.value;
                                evt.target.value = "__sozi_add__";
                                if (value === "__sozi_add_all__") {
                                    this.addAllLayers();
                                }
                                else {
                                    this.addLayer(value);
                                }
                            }
                        }, [
                            h("option", {value: "__sozi_add__", selected: "selected"}, _("Add layer")),
                            h("option", {value: "__sozi_add_all__"}, _("Add all layers")),
                            this.presentation.layers.slice().reverse()
                                .filter(layer => !layer.auto && this.defaultLayers.indexOf(layer) >= 0)
                                .map(layer => h("option", {value: layer.index}, layer.label))
                        ])
                    )
                ])
            ])
        ]),
        h("div.timeline-bottom-left", [
            h("table.timeline", [
                this.hasDefaultLayer ? h("tr", [
                    h("th.layer-icons", [
                        defaultLayersAreVisible ?
                            h("i.visibility.far.fa-eye", {
                                title: _("This layer is visible. Click to hide it."),
                                onclick: evt => this.toggleLayerVisibility(-1, evt)
                            }) :
                            h("i.visibility.far.fa-eye-slash", {
                                title: _("This layer is hidden. Click to show it."),
                                onclick: evt => this.toggleLayerVisibility(-1, evt)
                            }),
                        h("i.remove.fas.fa-times", {style: {visibility: "hidden"}})
                    ]),
                    h("th", {
                        className: "layer-label" + (this.defaultLayersAreSelected ? " selected" : ""),
                        onclick: evt => this.updateLayerSelection(-1, evt)
                    }, _("Default"))
                ]) : null,
                this.presentation.layers.slice().reverse()
                    .filter(layer => this.editableLayers.indexOf(layer) >= 0)
                    .map(layer => h("tr", [
                            h("th.layer-icons", [
                                layer.isVisible ?
                                    h("i.visibility.far.fa-eye", {
                                        title: _("This layer is visible. Click to hide it."),
                                        onclick: evt => this.toggleLayerVisibility(layer.index, evt)
                                    }) :
                                    h("i.visibility.far.fa-eye-slash", {
                                        title: _("This layer is hidden. Click to show it."),
                                        onclick: evt => this.toggleLayerVisibility(layer.index, evt)
                                    }),
                                h("i.remove.fas.fa-times", {
                                    title: _("Remove this layer"),
                                    onclick: () => this.removeLayer(layer.index)
                                })
                            ]),
                            h("th", {
                                className: "layer-label" + (this.selection.selectedLayers.indexOf(layer) >= 0 ? " selected" : ""),
                                onclick: evt => this.updateLayerSelection(layer.index, evt)
                            }, layer.label)
                        ])
                    ),
                h("tr", {style: {visibility: "collapse"}}, [
                    h("th.layer-icons"),
                    h("th.layer-label", _("Default"))
                ])
            ])
        ]),
        h("div.timeline-top-right", [
            h("table.timeline", [
                h("tr", this.presentation.frames.map((frame, frameIndex)  => h("th", {
                        className: "frame-index" +
                            (this.selection.selectedFrames.indexOf(frame) >= 0 ? " selected" : "") +
                            (frame === this.selection.currentFrame ? " current" : ""),
                        onclick: evt => this.updateFrameSelection(frameIndex, evt)
                    }, [
                        h("i.insert-before.fas.fa-arrow-circle-down", {
                            title: Jed.sprintf(_("Insert selection before frame %d"), frameIndex + 1),
                            onclick(evt) {
                                c.moveFrames(frameIndex);
                                evt.stopPropagation();
                            }
                        }),
                        h("i.insert-after.fas.fa-arrow-circle-down", {
                            title: Jed.sprintf(_("Insert selection after frame %d"), frameIndex + 1),
                            onclick(evt) {
                                c.moveFrames(frameIndex + 1);
                                evt.stopPropagation();
                            }
                        }),
                        (frameIndex + 1).toString()
                    ])
                )),
                h("tr",
                  this.presentation.frames.map((frame, frameIndex) => h("th", {
                            title: frame.title,
                            className: "frame-title" +
                                (this.selection.selectedFrames.indexOf(frame) >= 0 ? " selected" : "") +
                                (frame === this.selection.currentFrame ? " current" : ""),
                            onclick: evt => this.updateFrameSelection(frameIndex, evt)
                        }, frame.title)
                    )
                 )
            ])
        ]),
        h("div.timeline-bottom-right", {
            onscroll: evt => {
                this.container.querySelector(".timeline-top-right").scrollLeft = evt.target.scrollLeft;
                this.container.querySelector(".timeline-bottom-left").scrollTop = evt.target.scrollTop;
            }
        }, h("table.timeline", [
                this.hasDefaultLayer ? h("tr",
                    this.presentation.frames.map((frame, frameIndex) => h("td", {
                        className:
                            (this.defaultLayersAreSelected && this.selection.selectedFrames.indexOf(frame) >= 0 ? "selected" : "") +
                            (frame === this.selection.currentFrame ? " current" : "") +
                            (frameIndex > 0 && frame.layerProperties[this.defaultLayers[0].index].link ? " link" : "") +
                            (updateEven(frame, this.defaultLayers[0]) ? " even" : " odd"),
                        onclick: evt => this.updateLayerAndFrameSelection(-1, frameIndex, evt)
                    }))
                ) : null,
                this.presentation.layers.slice().reverse()
                    .filter(layer => this.editableLayers.indexOf(layer) >= 0)
                    .map(layer => h("tr",
                        this.presentation.frames.map((frame, frameIndex) => h("td", {
                            className:
                                (this.selection.selectedLayers.indexOf(layer) >= 0 && this.selection.selectedFrames.indexOf(frame) >= 0 ? "selected" : "") +
                                (frame === this.selection.currentFrame ? " current" : "") +
                                (frameIndex > 0 && frame.layerProperties[layer.index].link ? " link" : "") +
                                (updateEven(frame, layer) ? " even" : " odd"),
                            onclick: evt => this.updateLayerAndFrameSelection(layer.index, frameIndex, evt)
                        })
                    ))),
                h("tr.collapse",
                    this.presentation.frames.map(frame => h("td", frame.title))
                )
            ])
        )
    ]);
};
<|endoftext|>


<commit_before>
//= require "natives/box"

Ergo.declare('Ergo.widgets.Panel', 'Ergo.widgets.Box', {

	defaults: {
		cls: 'e-panel',
		components: {
			header: {
				weight: -10,
				html: '<header/>',
				components: {
					title: {
//						html: '<h2/>'
					}
					// buttons: {
						// cls: 'buttons',
						// layout: 'hbox',
						// defaultItem: {
							// etype: 'button-item'
						// }
					// }
				}
			},
			content: {
				weight: 0,
				cls: 'e-panel-content'

			},
			footer: {
				weight: 10,
				html: '<footer/>',
				hidden: true
//				state: 'hidden'
			}
		}

//		headerButtons: [],
//		footerButtons: []

		// set: {
			// 'title': function(s) { this.header.title.opt('text', s); }
		// }
	},

	$init: function(o) {
		this.$super(o);

//		Ergo.smart_override(o.components.header.components.buttons, {items: o.headerButtons});
	},

	setTitle: function(s) {
		this.header.title.opt('text', s);
	}

}, 'panel');
<commit_msg>+ class title for panel header<commit_after>
//= require "natives/box"

Ergo.declare('Ergo.widgets.Panel', 'Ergo.widgets.Box', {

	defaults: {
		cls: 'e-panel',
		components: {
			header: {
				weight: -10,
				html: '<header/>',
				components: {
					title: {
						cls: 'title'
//						html: '<h2/>'
					}
					// buttons: {
						// cls: 'buttons',
						// layout: 'hbox',
						// defaultItem: {
							// etype: 'button-item'
						// }
					// }
				}
			},
			content: {
				weight: 0,
				cls: 'e-panel-content'

			},
			footer: {
				weight: 10,
				html: '<footer/>',
				hidden: true
//				state: 'hidden'
			}
		}

//		headerButtons: [],
//		footerButtons: []

		// set: {
			// 'title': function(s) { this.header.title.opt('text', s); }
		// }
	},

	$init: function(o) {
		this.$super(o);

//		Ergo.smart_override(o.components.header.components.buttons, {items: o.headerButtons});
	},

	setTitle: function(s) {
		this.header.title.opt('text', s);
	}

}, 'panel');
<|endoftext|>


<commit_before>'use strict';

var Q = require('q');
var AbstractClientResource = require('./../abstract-client-resource');

/**
 * No-Op callback
 */
function noop() {}

/**
 * Process Definition Resource
 * @class
 * @memberof CamSDK.client.resource
 * @augments CamSDK.client.AbstractClientResource
 */
var ProcessDefinition = AbstractClientResource.extend(
/** @lends  CamSDK.client.resource.ProcessDefinition.prototype */
  {
  /**
   * Suspends the process definition instance
   *
   * @param  {Object.<String, *>} [params]
   * @param  {requestCallback}    [done]
   */
    suspend: function(params, done) {
    // allows to pass only a callback
      if (typeof params === 'function') {
        done = params;
        params = {};
      }
      params = params || {};
      done = done || noop;

      return this.http.post(this.path, {
        done: done
      });
    },

  /**
   * Retrieves the statistics of a process definition.
   *
   * @param  {Function} [done]
   */
    stats: function(done) {
      return this.http.post(this.path, {
        done: done || noop
      });
    },

  /**
   * Retrieves the BPMN 2.0 XML document of a process definition.
   *
   * @param  {Function} [done]
   */
  // xml: function(id, done) {
  //   return this.http.post(this.path + +'/xml', {
  //     done: done || noop
  //   });
  // },

  /**
   * Starts a process instance from a process definition.
   *
   * @param  {Object} [varname]
   * @param  {Function} [done]
   */
    start: function(done) {
      return this.http.post(this.path, {
        data: {},
        done: done
      });
    }
  },
/** @lends  CamSDK.client.resource.ProcessDefinition */
  {
  /**
   * API path for the process instance resource
   */
    path: 'process-definition',

  /**
   * Retrieve a single process definition
   *
   * @param  {uuid}     id    of the process definition to be requested
   * @param  {Function} done
   */
    get: function(id, done) {

    // var pointer = '';
    // if (data.key) {
    //   pointer = 'key/'+ data.key;
    // }
    // else if (data.id) {
    //   pointer = data.id;
    // }

      return this.http.get(this.path +'/'+ id, {
        done: done
      });
    },

  /**
   * Retrieve a single process definition
   *
   * @param  {String}   key    of the process definition to be requested
   * @param  {Function} done
   */
    getByKey: function(key, done) {
      return this.http.get(this.path +'/key/'+ key, {
        done: done
      });
    },

  /**
   * Get a list of process definitions
   * @param  {Object} params                        Query parameters as follow
   * @param  {String} [params.name]                 Filter by name.
   * @param  {String} [params.nameLike]             Filter by names that the parameter is a substring of.
   * @param  {String} [params.deploymentId]         Filter by the deployment the id belongs to.
   * @param  {String} [params.key]                  Filter by key, i.e. the id in the BPMN 2.0 XML. Exact match.
   * @param  {String} [params.keyLike]              Filter by keys that the parameter is a substring of.
   * @param  {String} [params.category]             Filter by category. Exact match.
   * @param  {String} [params.categoryLike]         Filter by categories that the parameter is a substring of.
   * @param  {String} [params.ver]                  Filter by version.
   * @param  {String} [params.latest]               Only include those process definitions that are latest versions.
   *                                                Values may be "true" or "false".
   * @param  {String} [params.resourceName]         Filter by the name of the process definition resource. Exact match.
   * @param  {String} [params.resourceNameLike]     Filter by names of those process definition resources that the parameter is a substring of.
   * @param  {String} [params.startableBy]          Filter by a user name who is allowed to start the process.
   * @param  {String} [params.active]               Only include active process definitions.
   *                                                Values may be "true" or "false".
   * @param  {String} [params.suspended]            Only include suspended process definitions.
   *                                                Values may be "true" or "false".
   * @param  {String} [params.incidentId]           Filter by the incident id.
   * @param  {String} [params.incidentType]         Filter by the incident type.
   * @param  {String} [params.incidentMessage]      Filter by the incident message. Exact match.
   * @param  {String} [params.incidentMessageLike]  Filter by the incident message that the parameter is a substring of.
   *
   * @param  {String} [params.sortBy]               Sort the results lexicographically by a given criterion.
   *                                                Valid values are category, "key", "id", "name", "version" and "deploymentId".
   *                                                Must be used in conjunction with the "sortOrder" parameter.
   *
   * @param  {String} [params.sortOrder]            Sort the results in a given order.
   *                                                Values may be asc for ascending "order" or "desc" for descending order.
   *                                                Must be used in conjunction with the sortBy parameter.
   *
   * @param  {Integer} [params.firstResult]         Pagination of results. Specifies the index of the first result to return.
   * @param  {Integer} [params.maxResults]          Pagination of results. Specifies the maximum number of results to return.
   *                                                Will return less results, if there are no more results left.

   * @param  {requestCallback} [done]
   *
   * @example
   * CamSDK.resource('process-definition').list({
   *   nameLike: 'Process'
   * }, function(err, results) {
   *   //
   * });
   */
    list: function() {
      return AbstractClientResource.list.apply(this, arguments);
    },

  /**
   * Fetch the variables of a process definition
   * @param  {Object.<String, *>} data
   * @param  {String}             [data.id]     of the process
   * @param  {String}             [data.key]    of the process
   * @param  {Array}              [data.names]  of variables to be fetched
   * @param  {Function}           [done]
   */
    formVariables: function(data, done) {
      var pointer = '';
      done = done || noop;
      if (data.key) {
        pointer = 'key/'+ data.key;
      }
      else if (data.id) {
        pointer = data.id;
      }
      else {
        var err = new Error('Process definition task variables needs either a key or an id.');
        done(err);
        return Q.reject(err);
      }

      var queryData = {
        deserializeValues: data.deserializeValues
      };

      if(data.names) {
        queryData.variableNames = (data.names || []).join(',');
      }

      return this.http.get(this.path +'/'+ pointer +'/form-variables', {
        data: queryData,
        done: done
      });
    },

  /**
   * Submit a form to start a process definition
   *
   * @param  {Object.<String, *>} data
   * @param  {String}             [data.key]            start the process-definition with this key
   * @param  {String}             [data.tenantId]       and the this tenant-id
   * @param  {String}             [data.id]             or: start the process-definition with this id
   * @param  {String}             [data.businessKey]    of the process to be set
   * @param  {Array}              [data.variables]      variables to be set
   * @param  {Function}           [done]
   */
    submitForm: function(data, done) {
      var pointer = '';
      done = done || noop;
      if (data.key) {
        pointer = 'key/'+ data.key;
        if (data.tenantId) {
          pointer += '/tenant-id/' + data.tenantId;
        }
      }
      else if (data.id) {
        pointer = data.id;
      }
      else {
        return done(new Error('Process definition task variables needs either a key or an id.'));
      }

      return this.http.post(this.path +'/'+ pointer +'/submit-form', {
        data: {
          businessKey : data.businessKey,
          variables: data.variables
        },
        done: done
      });
    },

  /**
   * Retrieves the form of a process definition.
   * @param  {Function} [done]
   */
    startForm: function(data, done) {
      var path = this.path +'/'+ (data.key ? 'key/'+ data.key : data.id) +'/startForm';
      return this.http.get(path, {
        done: done || noop
      });
    },

  /**
   * Retrieves the form of a process definition.
   * @param  {Function} [done]
   */
    xml: function(data, done) {
      var path = this.path +'/'+ (data.id ? data.id : 'key/'+ data.key) +'/xml';
      return this.http.get(path, {
        done: done || noop
      });
    },

  /**
   * Retrieves runtime statistics of a given process definition grouped by activities
   * @param  {Function} [done]
   */
    statistics: function(data, done) {
      var path = this.path +'/'+ (data.id ? data.id : 'key/'+ data.key) +'/statistics';
      return this.http.get(path, {
        data: data,
        done: done || noop
      });
    },

  /**
   * Submits the form of a process definition.
   *
   * @param  {Object} [data]
   * @param  {Function} [done]
   */
    submit: function(data, done) {
      var path = this.path;
      if (data.key) {
        path += '/key/'+ data.key;
      }
      else {
        path += '/'+ data.id;
      }
      path += '/submit-form';

      return this.http.post(path, {
        data: data,
        done: done
      });
    },

  /**
   * Suspends one or more process definitions
   *
   * @param  {String|String[]}    ids
   * @param  {Object.<String, *>} [params]
   * @param  {requestCallback}    [done]
   */
    suspend: function(ids, params, done) {
    // allows to pass only a callback
      if (typeof params === 'function') {
        done = params;
        params = {};
      }
      params = params || {};
      done = done || noop;
    // allows to pass a single ID
      ids = Array.isArray(ids) ? ids : [ids];

      return this.http.post(this.path, {
        done: done
      });
    },

  /**
   * Instantiates a given process definition.
   *
   * @param {Object} [params]
   * @param {String} [params.id]              The id of the process definition to be instantiated. Must be omitted if key is provided.
   * @param {String} [params.key]             The key of the process definition (the latest version thereof) to be instantiated. Must be omitted if id is provided.
   * @param {String} [params.tenantId]				The id of the tenant the process definition belongs to. Must be omitted if id is provided.
   * @param {String} [params.variables]       A JSON object containing the variables the process is to be initialized with. Each key corresponds to a variable name and each value to a variable value.
   * @param {String} [params.businessKey]     The business key the process instance is to be initialized with. The business key uniquely identifies the process instance in the context of the given process definition.
   * @param {String} [params.caseInstanceId]  The case instance id the process instance is to be initialized with.
   */
    start: function(params, done) {
      var url = this.path + '/';

      if (params.id) {
        url = url + params.id;
      } else {
        url = url + 'key/' + params.key;

        if (params.tenantId) {
          url = url + '/tenant-id/' + params.tenantId;
        }
      }

      return this.http.post(url + '/start', {
        data: params,
        done: done
      });
    }
  });

module.exports = ProcessDefinition;

<commit_msg>feat: add history time to live API to sdk<commit_after>'use strict';

var Q = require('q');
var AbstractClientResource = require('./../abstract-client-resource');

/**
 * No-Op callback
 */
function noop() {}

/**
 * Process Definition Resource
 * @class
 * @memberof CamSDK.client.resource
 * @augments CamSDK.client.AbstractClientResource
 */
var ProcessDefinition = AbstractClientResource.extend(
/** @lends  CamSDK.client.resource.ProcessDefinition.prototype */
  {
  /**
   * Suspends the process definition instance
   *
   * @param  {Object.<String, *>} [params]
   * @param  {requestCallback}    [done]
   */
    suspend: function(params, done) {
    // allows to pass only a callback
      if (typeof params === 'function') {
        done = params;
        params = {};
      }
      params = params || {};
      done = done || noop;

      return this.http.post(this.path, {
        done: done
      });
    },

  /**
   * Retrieves the statistics of a process definition.
   *
   * @param  {Function} [done]
   */
    stats: function(done) {
      return this.http.post(this.path, {
        done: done || noop
      });
    },

  /**
   * Retrieves the BPMN 2.0 XML document of a process definition.
   *
   * @param  {Function} [done]
   */
  // xml: function(id, done) {
  //   return this.http.post(this.path + +'/xml', {
  //     done: done || noop
  //   });
  // },

  /**
   * Starts a process instance from a process definition.
   *
   * @param  {Object} [varname]
   * @param  {Function} [done]
   */
    start: function(done) {
      return this.http.post(this.path, {
        data: {},
        done: done
      });
    }
  },
/** @lends  CamSDK.client.resource.ProcessDefinition */
  {
  /**
   * API path for the process instance resource
   */
    path: 'process-definition',

  /**
   * Retrieve a single process definition
   *
   * @param  {uuid}     id    of the process definition to be requested
   * @param  {Function} done
   */
    get: function(id, done) {

    // var pointer = '';
    // if (data.key) {
    //   pointer = 'key/'+ data.key;
    // }
    // else if (data.id) {
    //   pointer = data.id;
    // }

      return this.http.get(this.path +'/'+ id, {
        done: done
      });
    },

  /**
   * Retrieve a single process definition
   *
   * @param  {String}   key    of the process definition to be requested
   * @param  {Function} done
   */
    getByKey: function(key, done) {
      return this.http.get(this.path +'/key/'+ key, {
        done: done
      });
    },

  /**
   * Get a list of process definitions
   * @param  {Object} params                        Query parameters as follow
   * @param  {String} [params.name]                 Filter by name.
   * @param  {String} [params.nameLike]             Filter by names that the parameter is a substring of.
   * @param  {String} [params.deploymentId]         Filter by the deployment the id belongs to.
   * @param  {String} [params.key]                  Filter by key, i.e. the id in the BPMN 2.0 XML. Exact match.
   * @param  {String} [params.keyLike]              Filter by keys that the parameter is a substring of.
   * @param  {String} [params.category]             Filter by category. Exact match.
   * @param  {String} [params.categoryLike]         Filter by categories that the parameter is a substring of.
   * @param  {String} [params.ver]                  Filter by version.
   * @param  {String} [params.latest]               Only include those process definitions that are latest versions.
   *                                                Values may be "true" or "false".
   * @param  {String} [params.resourceName]         Filter by the name of the process definition resource. Exact match.
   * @param  {String} [params.resourceNameLike]     Filter by names of those process definition resources that the parameter is a substring of.
   * @param  {String} [params.startableBy]          Filter by a user name who is allowed to start the process.
   * @param  {String} [params.active]               Only include active process definitions.
   *                                                Values may be "true" or "false".
   * @param  {String} [params.suspended]            Only include suspended process definitions.
   *                                                Values may be "true" or "false".
   * @param  {String} [params.incidentId]           Filter by the incident id.
   * @param  {String} [params.incidentType]         Filter by the incident type.
   * @param  {String} [params.incidentMessage]      Filter by the incident message. Exact match.
   * @param  {String} [params.incidentMessageLike]  Filter by the incident message that the parameter is a substring of.
   *
   * @param  {String} [params.sortBy]               Sort the results lexicographically by a given criterion.
   *                                                Valid values are category, "key", "id", "name", "version" and "deploymentId".
   *                                                Must be used in conjunction with the "sortOrder" parameter.
   *
   * @param  {String} [params.sortOrder]            Sort the results in a given order.
   *                                                Values may be asc for ascending "order" or "desc" for descending order.
   *                                                Must be used in conjunction with the sortBy parameter.
   *
   * @param  {Integer} [params.firstResult]         Pagination of results. Specifies the index of the first result to return.
   * @param  {Integer} [params.maxResults]          Pagination of results. Specifies the maximum number of results to return.
   *                                                Will return less results, if there are no more results left.

   * @param  {requestCallback} [done]
   *
   * @example
   * CamSDK.resource('process-definition').list({
   *   nameLike: 'Process'
   * }, function(err, results) {
   *   //
   * });
   */
    list: function() {
      return AbstractClientResource.list.apply(this, arguments);
    },

  /**
   * Fetch the variables of a process definition
   * @param  {Object.<String, *>} data
   * @param  {String}             [data.id]     of the process
   * @param  {String}             [data.key]    of the process
   * @param  {Array}              [data.names]  of variables to be fetched
   * @param  {Function}           [done]
   */
    formVariables: function(data, done) {
      var pointer = '';
      done = done || noop;
      if (data.key) {
        pointer = 'key/'+ data.key;
      }
      else if (data.id) {
        pointer = data.id;
      }
      else {
        var err = new Error('Process definition task variables needs either a key or an id.');
        done(err);
        return Q.reject(err);
      }

      var queryData = {
        deserializeValues: data.deserializeValues
      };

      if(data.names) {
        queryData.variableNames = (data.names || []).join(',');
      }

      return this.http.get(this.path +'/'+ pointer +'/form-variables', {
        data: queryData,
        done: done
      });
    },

  /**
   * Submit a form to start a process definition
   *
   * @param  {Object.<String, *>} data
   * @param  {String}             [data.key]            start the process-definition with this key
   * @param  {String}             [data.tenantId]       and the this tenant-id
   * @param  {String}             [data.id]             or: start the process-definition with this id
   * @param  {String}             [data.businessKey]    of the process to be set
   * @param  {Array}              [data.variables]      variables to be set
   * @param  {Function}           [done]
   */
    submitForm: function(data, done) {
      var pointer = '';
      done = done || noop;
      if (data.key) {
        pointer = 'key/'+ data.key;
        if (data.tenantId) {
          pointer += '/tenant-id/' + data.tenantId;
        }
      }
      else if (data.id) {
        pointer = data.id;
      }
      else {
        return done(new Error('Process definition task variables needs either a key or an id.'));
      }

      return this.http.post(this.path +'/'+ pointer +'/submit-form', {
        data: {
          businessKey : data.businessKey,
          variables: data.variables
        },
        done: done
      });
    },

  /**
   * Retrieves the form of a process definition.
   * @param  {Function} [done]
   */
    startForm: function(data, done) {
      var path = this.path +'/'+ (data.key ? 'key/'+ data.key : data.id) +'/startForm';
      return this.http.get(path, {
        done: done || noop
      });
    },

  /**
   * Retrieves the form of a process definition.
   * @param  {Function} [done]
   */
    xml: function(data, done) {
      var path = this.path +'/'+ (data.id ? data.id : 'key/'+ data.key) +'/xml';
      return this.http.get(path, {
        done: done || noop
      });
    },

  /**
   * Retrieves runtime statistics of a given process definition grouped by activities
   * @param  {Function} [done]
   */
    statistics: function(data, done) {
      var path = this.path +'/'+ (data.id ? data.id : 'key/'+ data.key) +'/statistics';
      return this.http.get(path, {
        data: data,
        done: done || noop
      });
    },

  /**
   * Submits the form of a process definition.
   *
   * @param  {Object} [data]
   * @param  {Function} [done]
   */
    submit: function(data, done) {
      var path = this.path;
      if (data.key) {
        path += '/key/'+ data.key;
      }
      else {
        path += '/'+ data.id;
      }
      path += '/submit-form';

      return this.http.post(path, {
        data: data,
        done: done
      });
    },

  /**
   * Suspends one or more process definitions
   *
   * @param  {String|String[]}    ids
   * @param  {Object.<String, *>} [params]
   * @param  {requestCallback}    [done]
   */
    suspend: function(ids, params, done) {
    // allows to pass only a callback
      if (typeof params === 'function') {
        done = params;
        params = {};
      }
      params = params || {};
      done = done || noop;
    // allows to pass a single ID
      ids = Array.isArray(ids) ? ids : [ids];

      return this.http.post(this.path, {
        done: done
      });
    },

  /**
   * Instantiates a given process definition.
   *
   * @param {Object} [params]
   * @param {String} [params.id]              The id of the process definition to be instantiated. Must be omitted if key is provided.
   * @param {String} [params.key]             The key of the process definition (the latest version thereof) to be instantiated. Must be omitted if id is provided.
   * @param {String} [params.tenantId]				The id of the tenant the process definition belongs to. Must be omitted if id is provided.
   * @param {String} [params.variables]       A JSON object containing the variables the process is to be initialized with. Each key corresponds to a variable name and each value to a variable value.
   * @param {String} [params.businessKey]     The business key the process instance is to be initialized with. The business key uniquely identifies the process instance in the context of the given process definition.
   * @param {String} [params.caseInstanceId]  The case instance id the process instance is to be initialized with.
   */
    start: function(params, done) {
      var url = this.path + '/';

      if (params.id) {
        url = url + params.id;
      } else {
        url = url + 'key/' + params.key;

        if (params.tenantId) {
          url = url + '/tenant-id/' + params.tenantId;
        }
      }

      return this.http.post(url + '/start', {
        data: params,
        done: done
      });
    },

    /**
    * Instantiates a given process definition.

    * @param {String} [id]                        The id of the process definition to activate or suspend.
    * @param {Object} [params]
    * @param {Number} [params.historyTimeToLive]  New value for historyTimeToLive field of process definition. Can be null.
    */
    updateHistoryTimeToLive: function(id, params, done) {
      var url = this.path + '/' + id + '/history-time-to-live';

      return this.http.put(url, {
        data: params,
        done: done
      });
    }
  });

module.exports = ProcessDefinition;
<|endoftext|>


<commit_before>
/**
 * Encodes data in bencode.
 *
 * @param  {Buffer|Array|String|Object|Number} data
 * @return {String}
 */
function encode( data ) {

  if( Buffer.isBuffer( data ) )
    return encode.bytes( data )

  switch( typeof data ) {
    case 'string':
      return encode.bytes( data )
      break
    case 'number':
      return encode.number( data )
      break
    case 'object':
      return data.constructor === Array
        ? encode.list( data )
        : encode.dict( data )
      break
  }

}

encode.bytes = function( data ) {
  return data.length + ':' + data
}

encode.number = function( data ) {
  return 'i' + data + 'e'
}

encode.dict = function( data ) {

  var dict = 'd'

  for( var k in data ) {
    dict += encode( k ) + encode( data[k] )
  }

  return dict + 'e'

}

encode.list = function( data ) {

  var i = 0
  var c = data.length
  var lst = 'l'

  for( ; i < c; i++ ) {
    lst += encode( data[i] )
  }

  return lst + 'e'

}

/**
 * Decodes bencoded data.
 *
 * @param  {Buffer} data
 * @param  {String} encoding
 * @return {Object|Array|Buffer|String|Number}
 */
function decode( data, encoding ) {

  if( !(this instanceof decode) ) {
    return new decode( data, encoding )
  }

  this.encoding = encoding || null

  this.data = !( data instanceof Buffer )
    ? new Buffer( data )
    : data

  return this.next()

}

decode.prototype = {

  next: function() {

    switch( this.data[0] ) {
      case 0x64: return this.dictionary()
      case 0x6C: return this.list()
      case 0x69: return this.integer()
      default:   return this.bytes()
    }

  },

  find: function( chr ) {

    var i = 0
    var c = this.data.length
    var d = this.data

    while( i < c ) {
      if( d[i] === chr )
        return i
      i++
    }

    return -1

  },

  forward: function( index ) {
    this.data = this.data.slice(
      index, this.data.length
    )
  },

  dictionary: function() {

    this.forward( 1 )

    var dict = {}

    while( this.data[0] !== 0x65 ) {
      dict[ this.next() ] = this.next()
    }

    this.forward( 1 )

    return dict

  },

  list: function() {

    this.forward( 1 )

    var lst = []

    while( this.data[0] !== 0x65 ) {
      lst.push( this.next() )
    }

    this.forward( 1 )

    return lst

  },

  integer: function() {

    var end    = this.find( 0x65 )
    var number = this.data.slice( 1, end )

    this.forward( end + 1 )

    return +number

  },

  bytes: function() {

    var sep    = this.find( 0x3A )
    var length = +this.data.slice( 0, sep ).toString()
    var sepl   = sep + 1 + length
    var bytes  = this.data.slice( sep + 1, sepl )

    this.forward( sepl )

    return this.encoding
      ? bytes.toString( this.encoding )
      : bytes

  }

}

// Expose methods
exports.encode = encode
exports.decode = decode
<commit_msg>Updated bencode.js: Got rid of self instantiation in decode()<commit_after>
/**
 * Encodes data in bencode.
 *
 * @param  {Buffer|Array|String|Object|Number} data
 * @return {String}
 */
function encode( data ) {

  if( Buffer.isBuffer( data ) )
    return encode.bytes( data )

  switch( typeof data ) {
    case 'string':
      return encode.bytes( data )
      break
    case 'number':
      return encode.number( data )
      break
    case 'object':
      return data.constructor === Array
        ? encode.list( data )
        : encode.dict( data )
      break
  }

}

encode.bytes = function( data ) {
  return data.length + ':' + data
}

encode.number = function( data ) {
  return 'i' + data + 'e'
}

encode.dict = function( data ) {

  var dict = 'd'

  for( var k in data ) {
    dict += encode( k ) + encode( data[k] )
  }

  return dict + 'e'

}

encode.list = function( data ) {

  var i = 0
  var c = data.length
  var lst = 'l'

  for( ; i < c; i++ ) {
    lst += encode( data[i] )
  }

  return lst + 'e'

}

/**
 * Decodes bencoded data.
 *
 * @param  {Buffer} data
 * @param  {String} encoding
 * @return {Object|Array|Buffer|String|Number}
 */
function decode( data, encoding ) {

  decode.encoding = encoding || null

  decode.data = !( data instanceof Buffer )
    ? new Buffer( data )
    : data

  return decode.next()

}

decode.encoding = null
decode.data = null

decode.next = function() {

  switch( decode.data[0] ) {
    case 0x64: return decode.dictionary()
    case 0x6C: return decode.list()
    case 0x69: return decode.integer()
    default:   return decode.bytes()
  }

}

decode.find = function( chr ) {

  var i = 0
  var c = decode.data.length
  var d = decode.data

  while( i < c ) {
    if( d[i] === chr )
      return i
    i++
  }

  return -1

}

decode.forward = function( index ) {
  decode.data = decode.data.slice(
    index, decode.data.length
  )
}

decode.dictionary = function() {

  decode.forward( 1 )

  var dict = {}

  while( decode.data[0] !== 0x65 ) {
    dict[ decode.next() ] = decode.next()
  }

  decode.forward( 1 )

  return dict

}

decode.list = function() {

  decode.forward( 1 )

  var lst = []

  while( decode.data[0] !== 0x65 ) {
    lst.push( decode.next() )
  }

  decode.forward( 1 )

  return lst

}

decode.integer = function() {

  var end    = decode.find( 0x65 )
  var number = decode.data.slice( 1, end )

  decode.forward( end + 1 )

  return +number

}

decode.bytes = function() {

  var sep    = decode.find( 0x3A )
  var length = +decode.data.slice( 0, sep ).toString()
  var sepl   = sep + 1 + length
  var bytes  = decode.data.slice( sep + 1, sepl )

  decode.forward( sepl )

  return decode.encoding
    ? bytes.toString( decode.encoding )
    : bytes

}

// Expose methods
exports.encode = encode
exports.decode = decode
<|endoftext|>


<commit_before>/**
 * node-compress-commons
 *
 * Copyright (c) 2014 Chris Talkington, contributors.
 * Licensed under the MIT license.
 * https://github.com/archiverjs/node-compress-commons/blob/master/LICENSE-MIT
 */
var inherits = require('util').inherits;
var crc32 = require('buffer-crc32');
var CRC32Stream = require('crc32-stream');
var DeflateCRC32Stream = CRC32Stream.DeflateCRC32Stream;

var ArchiveOutputStream = require('../archive-output-stream');
var ZipArchiveEntry = require('./zip-archive-entry');
var GeneralPurposeBit = require('./general-purpose-bit');

var constants = require('./constants');
var util = require('../../util');
var zipUtil = require('./util');

var ZipArchiveOutputStream = module.exports = function(options) {
  if (!(this instanceof ZipArchiveOutputStream)) {
    return new ZipArchiveOutputStream(options);
  }

  options = this.options = this._defaults(options);

  ArchiveOutputStream.call(this, options);

  this._entry = null;
  this._entries = [];
  this._archive = {
    centralLength: 0,
    centralOffset: 0,
    comment: '',
    finish: false,
    finished: false,
    processing: false,
    forceZip64: options.forceZip64,
    forceLocalTime: options.forceLocalTime
  };
};

inherits(ZipArchiveOutputStream, ArchiveOutputStream);

ZipArchiveOutputStream.prototype._afterAppend = function(ae) {
  this._entries.push(ae);

  if (ae.getGeneralPurposeBit().usesDataDescriptor()) {
    this._writeDataDescriptor(ae);
  }

  this._archive.processing = false;
  this._entry = null;

  if (this._archive.finish && !this._archive.finished) {
    this._finish();
  }
};

ZipArchiveOutputStream.prototype._appendBuffer = function(ae, source, callback) {
  if (source.length === 0) {
    ae.setMethod(constants.METHOD_STORED);
  }

  var method = ae.getMethod();

  if (method === constants.METHOD_STORED) {
    ae.setSize(source.length);
    ae.setCompressedSize(source.length);
    ae.setCrc(crc32.unsigned(source));
  }

  this._writeLocalFileHeader(ae);

  if (method === constants.METHOD_STORED) {
    this.write(source);
    this._afterAppend(ae);
    callback(null, ae);
    return;
  } else if (method === constants.METHOD_DEFLATED) {
    this._smartStream(ae, callback).end(source);
    return;
  } else {
    callback(new Error('compression method ' + method + ' not implemented'));
    return;
  }
};

ZipArchiveOutputStream.prototype._appendStream = function(ae, source, callback) {
  ae.getGeneralPurposeBit().useDataDescriptor(true);
  ae.setVersionNeededToExtract(constants.MIN_VERSION_DATA_DESCRIPTOR);

  this._writeLocalFileHeader(ae);

  var smart = this._smartStream(ae, callback);
  source.once('error', function(err) {
    smart.emit('error', err);
    smart.end();
  })
  source.pipe(smart);
};

ZipArchiveOutputStream.prototype._defaults = function(o) {
  if (typeof o !== 'object') {
    o = {};
  }

  if (typeof o.zlib !== 'object') {
    o.zlib = {};
  }

  if (typeof o.zlib.level !== 'number') {
    o.zlib.level = constants.ZLIB_BEST_SPEED;
  }

  o.forceZip64 = !!o.forceZip64;
  o.forceLocalTime = !!o.forceLocalTime;

  return o;
};

ZipArchiveOutputStream.prototype._finish = function() {
  this._archive.centralOffset = this.offset;

  this._entries.forEach(function(ae) {
    this._writeCentralFileHeader(ae);
  }.bind(this));

  this._archive.centralLength = this.offset - this._archive.centralOffset;

  if (this.isZip64()) {
    this._writeCentralDirectoryZip64();
  }

  this._writeCentralDirectoryEnd();

  this._archive.processing = false;
  this._archive.finish = true;
  this._archive.finished = true;
  this.end();
};

ZipArchiveOutputStream.prototype._normalizeEntry = function(ae) {
  if (ae.getMethod() === -1) {
    ae.setMethod(constants.METHOD_DEFLATED);
  }

  if (ae.getMethod() === constants.METHOD_DEFLATED) {
    ae.getGeneralPurposeBit().useDataDescriptor(true);
    ae.setVersionNeededToExtract(constants.MIN_VERSION_DATA_DESCRIPTOR);
  }

  if (ae.getTime() === -1) {
    ae.setTime(new Date(), this._archive.forceLocalTime);
  }

  ae._offsets = {
    file: 0,
    data: 0,
    contents: 0,
  };
};

ZipArchiveOutputStream.prototype._smartStream = function(ae, callback) {
  var deflate = ae.getMethod() === constants.METHOD_DEFLATED;
  var process = deflate ? new DeflateCRC32Stream(this.options.zlib) : new CRC32Stream();
  var error = null;

  function handleStuff() {
    ae.setCrc(process.digest());
    ae.setSize(process.size());
    ae.setCompressedSize(process.size(true));
    this._afterAppend(ae);
    callback(error, ae);
  }

  process.once('end', handleStuff.bind(this));
  process.once('error', function(err) {
    error = err;
  });

  process.pipe(this, { end: false });

  return process;
};

ZipArchiveOutputStream.prototype._writeCentralDirectoryEnd = function() {
  var records = this._entries.length;
  var size = this._archive.centralLength;
  var offset = this._archive.centralOffset;

  if (this.isZip64()) {
    records = constants.ZIP64_MAGIC_SHORT;
    size = constants.ZIP64_MAGIC;
    offset = constants.ZIP64_MAGIC;
  }

  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_EOCD));

  // disk numbers
  this.write(constants.SHORT_ZERO);
  this.write(constants.SHORT_ZERO);

  // number of entries
  this.write(zipUtil.getShortBytes(records));
  this.write(zipUtil.getShortBytes(records));

  // length and location of CD
  this.write(zipUtil.getLongBytes(size));
  this.write(zipUtil.getLongBytes(offset));

  // archive comment
  var comment = this.getComment();
  var commentLength = Buffer.byteLength(comment);
  this.write(zipUtil.getShortBytes(commentLength));
  this.write(comment);
};

ZipArchiveOutputStream.prototype._writeCentralDirectoryZip64 = function() {
  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_ZIP64_EOCD));

  // size of the ZIP64 EOCD record
  this.write(zipUtil.getEightBytes(56));

  // version made by
  this.write(zipUtil.getShortBytes(constants.MIN_VERSION_ZIP64));

  // version to extract
  this.write(zipUtil.getShortBytes(constants.MIN_VERSION_ZIP64));

  // disk numbers
  this.write(constants.LONG_ZERO);
  this.write(constants.LONG_ZERO);

  // number of entries
  this.write(zipUtil.getEightBytes(this._entries.length));
  this.write(zipUtil.getEightBytes(this._entries.length));

  // length and location of CD
  this.write(zipUtil.getEightBytes(this._archive.centralLength));
  this.write(zipUtil.getEightBytes(this._archive.centralOffset));

  // extensible data sector
  // not implemented at this time

  // end of central directory locator
  this.write(zipUtil.getLongBytes(constants.SIG_ZIP64_EOCD_LOC));

  // disk number holding the ZIP64 EOCD record
  this.write(constants.LONG_ZERO);

  // relative offset of the ZIP64 EOCD record
  this.write(zipUtil.getEightBytes(this._archive.centralOffset + this._archive.centralLength));

  // total number of disks
  this.write(zipUtil.getLongBytes(1));
};

ZipArchiveOutputStream.prototype._writeCentralFileHeader = function(ae) {
  var gpb = ae.getGeneralPurposeBit();
  var method = ae.getMethod();
  var offsets = ae._offsets;

  var size = ae.getSize();
  var compressedSize = ae.getCompressedSize();

  if (ae.isZip64() || offsets.file > constants.ZIP64_MAGIC) {
    size = constants.ZIP64_MAGIC;
    compressedSize = constants.ZIP64_MAGIC;

    ae.setVersionNeededToExtract(constants.MIN_VERSION_ZIP64);

    var extraBuf = Buffer.concat([
      zipUtil.getShortBytes(constants.ZIP64_EXTRA_ID),
      zipUtil.getShortBytes(24),
      zipUtil.getEightBytes(ae.getSize()),
      zipUtil.getEightBytes(ae.getCompressedSize()),
      zipUtil.getEightBytes(offsets.file)
    ], 28);

    ae.setExtra(extraBuf);
  }

  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_CFH));

  // version made by
  this.write(zipUtil.getShortBytes((ae.getPlatform() << 8) | constants.VERSION_MADEBY));

  // version to extract and general bit flag
  this.write(zipUtil.getShortBytes(ae.getVersionNeededToExtract()));
  this.write(gpb.encode());

  // compression method
  this.write(zipUtil.getShortBytes(method));

  // datetime
  this.write(zipUtil.getLongBytes(ae.getTimeDos()));

  // crc32 checksum
  this.write(zipUtil.getLongBytes(ae.getCrc()));

  // sizes
  this.write(zipUtil.getLongBytes(compressedSize));
  this.write(zipUtil.getLongBytes(size));

  var name = ae.getName();
  var comment = ae.getComment();
  var extra = ae.getCentralDirectoryExtra();

  if (gpb.usesUTF8ForNames()) {
    name = new Buffer(name);
    comment = new Buffer(comment);
  }

  // name length
  this.write(zipUtil.getShortBytes(name.length));

  // extra length
  this.write(zipUtil.getShortBytes(extra.length));

  // comments length
  this.write(zipUtil.getShortBytes(comment.length));

  // disk number start
  this.write(constants.SHORT_ZERO);

  // internal attributes
  this.write(zipUtil.getShortBytes(ae.getInternalAttributes()));

  // external attributes
  this.write(zipUtil.getLongBytes(ae.getExternalAttributes()));

  // relative offset of LFH
  if (offsets.file > constants.ZIP64_MAGIC) {
    this.write(zipUtil.getLongBytes(constants.ZIP64_MAGIC));
  } else {
    this.write(zipUtil.getLongBytes(offsets.file));
  }

  // name
  this.write(name);

  // extra
  this.write(extra);

  // comment
  this.write(comment);
};

ZipArchiveOutputStream.prototype._writeDataDescriptor = function(ae) {
  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_DD));

  // crc32 checksum
  this.write(zipUtil.getLongBytes(ae.getCrc()));

  // sizes
  if (ae.isZip64()) {
    this.write(zipUtil.getEightBytes(ae.getCompressedSize()));
    this.write(zipUtil.getEightBytes(ae.getSize()));
  } else {
    this.write(zipUtil.getLongBytes(ae.getCompressedSize()));
    this.write(zipUtil.getLongBytes(ae.getSize()));
  }
};

ZipArchiveOutputStream.prototype._writeLocalFileHeader = function(ae) {
  var gpb = ae.getGeneralPurposeBit();
  var method = ae.getMethod();
  var name = ae.getName();
  var extra = ae.getLocalFileDataExtra();

  if (ae.isZip64()) {
    gpb.useDataDescriptor(true);
    ae.setVersionNeededToExtract(constants.MIN_VERSION_ZIP64);
  }

  if (gpb.usesUTF8ForNames()) {
    name = new Buffer(name);
  }

  ae._offsets.file = this.offset;

  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_LFH));

  // version to extract and general bit flag
  this.write(zipUtil.getShortBytes(ae.getVersionNeededToExtract()));
  this.write(gpb.encode());

  // compression method
  this.write(zipUtil.getShortBytes(method));

  // datetime
  this.write(zipUtil.getLongBytes(ae.getTimeDos()));

  ae._offsets.data = this.offset;

  // crc32 checksum and sizes
  if (gpb.usesDataDescriptor()) {
    this.write(constants.LONG_ZERO);
    this.write(constants.LONG_ZERO);
    this.write(constants.LONG_ZERO);
  } else {
    this.write(zipUtil.getLongBytes(ae.getCrc()));
    this.write(zipUtil.getLongBytes(ae.getCompressedSize()));
    this.write(zipUtil.getLongBytes(ae.getSize()));
  }

  // name length
  this.write(zipUtil.getShortBytes(name.length));

  // extra length
  this.write(zipUtil.getShortBytes(extra.length));

  // name
  this.write(name);

  // extra
  this.write(extra);

  ae._offsets.contents = this.offset;
};

ZipArchiveOutputStream.prototype.getComment = function(comment) {
  return this._archive.comment !== null ? this._archive.comment : '';
};

ZipArchiveOutputStream.prototype.isZip64 = function() {
  return this._archive.forceZip64 || this._entries.length > constants.ZIP64_MAGIC_SHORT || this._archive.centralLength > constants.ZIP64_MAGIC || this._archive.centralOffset > constants.ZIP64_MAGIC;
};

ZipArchiveOutputStream.prototype.setComment = function(comment) {
  this._archive.comment = comment;
};<commit_msg>convert digest buffer to a number.<commit_after>/**
 * node-compress-commons
 *
 * Copyright (c) 2014 Chris Talkington, contributors.
 * Licensed under the MIT license.
 * https://github.com/archiverjs/node-compress-commons/blob/master/LICENSE-MIT
 */
var inherits = require('util').inherits;
var crc32 = require('buffer-crc32');
var CRC32Stream = require('crc32-stream');
var DeflateCRC32Stream = CRC32Stream.DeflateCRC32Stream;

var ArchiveOutputStream = require('../archive-output-stream');
var ZipArchiveEntry = require('./zip-archive-entry');
var GeneralPurposeBit = require('./general-purpose-bit');

var constants = require('./constants');
var util = require('../../util');
var zipUtil = require('./util');

var ZipArchiveOutputStream = module.exports = function(options) {
  if (!(this instanceof ZipArchiveOutputStream)) {
    return new ZipArchiveOutputStream(options);
  }

  options = this.options = this._defaults(options);

  ArchiveOutputStream.call(this, options);

  this._entry = null;
  this._entries = [];
  this._archive = {
    centralLength: 0,
    centralOffset: 0,
    comment: '',
    finish: false,
    finished: false,
    processing: false,
    forceZip64: options.forceZip64,
    forceLocalTime: options.forceLocalTime
  };
};

inherits(ZipArchiveOutputStream, ArchiveOutputStream);

ZipArchiveOutputStream.prototype._afterAppend = function(ae) {
  this._entries.push(ae);

  if (ae.getGeneralPurposeBit().usesDataDescriptor()) {
    this._writeDataDescriptor(ae);
  }

  this._archive.processing = false;
  this._entry = null;

  if (this._archive.finish && !this._archive.finished) {
    this._finish();
  }
};

ZipArchiveOutputStream.prototype._appendBuffer = function(ae, source, callback) {
  if (source.length === 0) {
    ae.setMethod(constants.METHOD_STORED);
  }

  var method = ae.getMethod();

  if (method === constants.METHOD_STORED) {
    ae.setSize(source.length);
    ae.setCompressedSize(source.length);
    ae.setCrc(crc32.unsigned(source));
  }

  this._writeLocalFileHeader(ae);

  if (method === constants.METHOD_STORED) {
    this.write(source);
    this._afterAppend(ae);
    callback(null, ae);
    return;
  } else if (method === constants.METHOD_DEFLATED) {
    this._smartStream(ae, callback).end(source);
    return;
  } else {
    callback(new Error('compression method ' + method + ' not implemented'));
    return;
  }
};

ZipArchiveOutputStream.prototype._appendStream = function(ae, source, callback) {
  ae.getGeneralPurposeBit().useDataDescriptor(true);
  ae.setVersionNeededToExtract(constants.MIN_VERSION_DATA_DESCRIPTOR);

  this._writeLocalFileHeader(ae);

  var smart = this._smartStream(ae, callback);
  source.once('error', function(err) {
    smart.emit('error', err);
    smart.end();
  })
  source.pipe(smart);
};

ZipArchiveOutputStream.prototype._defaults = function(o) {
  if (typeof o !== 'object') {
    o = {};
  }

  if (typeof o.zlib !== 'object') {
    o.zlib = {};
  }

  if (typeof o.zlib.level !== 'number') {
    o.zlib.level = constants.ZLIB_BEST_SPEED;
  }

  o.forceZip64 = !!o.forceZip64;
  o.forceLocalTime = !!o.forceLocalTime;

  return o;
};

ZipArchiveOutputStream.prototype._finish = function() {
  this._archive.centralOffset = this.offset;

  this._entries.forEach(function(ae) {
    this._writeCentralFileHeader(ae);
  }.bind(this));

  this._archive.centralLength = this.offset - this._archive.centralOffset;

  if (this.isZip64()) {
    this._writeCentralDirectoryZip64();
  }

  this._writeCentralDirectoryEnd();

  this._archive.processing = false;
  this._archive.finish = true;
  this._archive.finished = true;
  this.end();
};

ZipArchiveOutputStream.prototype._normalizeEntry = function(ae) {
  if (ae.getMethod() === -1) {
    ae.setMethod(constants.METHOD_DEFLATED);
  }

  if (ae.getMethod() === constants.METHOD_DEFLATED) {
    ae.getGeneralPurposeBit().useDataDescriptor(true);
    ae.setVersionNeededToExtract(constants.MIN_VERSION_DATA_DESCRIPTOR);
  }

  if (ae.getTime() === -1) {
    ae.setTime(new Date(), this._archive.forceLocalTime);
  }

  ae._offsets = {
    file: 0,
    data: 0,
    contents: 0,
  };
};

ZipArchiveOutputStream.prototype._smartStream = function(ae, callback) {
  var deflate = ae.getMethod() === constants.METHOD_DEFLATED;
  var process = deflate ? new DeflateCRC32Stream(this.options.zlib) : new CRC32Stream();
  var error = null;

  function handleStuff() {
    var digest = process.digest().readUInt32BE(0);
    ae.setCrc(digest);
    ae.setSize(process.size());
    ae.setCompressedSize(process.size(true));
    this._afterAppend(ae);
    callback(error, ae);
  }

  process.once('end', handleStuff.bind(this));
  process.once('error', function(err) {
    error = err;
  });

  process.pipe(this, { end: false });

  return process;
};

ZipArchiveOutputStream.prototype._writeCentralDirectoryEnd = function() {
  var records = this._entries.length;
  var size = this._archive.centralLength;
  var offset = this._archive.centralOffset;

  if (this.isZip64()) {
    records = constants.ZIP64_MAGIC_SHORT;
    size = constants.ZIP64_MAGIC;
    offset = constants.ZIP64_MAGIC;
  }

  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_EOCD));

  // disk numbers
  this.write(constants.SHORT_ZERO);
  this.write(constants.SHORT_ZERO);

  // number of entries
  this.write(zipUtil.getShortBytes(records));
  this.write(zipUtil.getShortBytes(records));

  // length and location of CD
  this.write(zipUtil.getLongBytes(size));
  this.write(zipUtil.getLongBytes(offset));

  // archive comment
  var comment = this.getComment();
  var commentLength = Buffer.byteLength(comment);
  this.write(zipUtil.getShortBytes(commentLength));
  this.write(comment);
};

ZipArchiveOutputStream.prototype._writeCentralDirectoryZip64 = function() {
  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_ZIP64_EOCD));

  // size of the ZIP64 EOCD record
  this.write(zipUtil.getEightBytes(56));

  // version made by
  this.write(zipUtil.getShortBytes(constants.MIN_VERSION_ZIP64));

  // version to extract
  this.write(zipUtil.getShortBytes(constants.MIN_VERSION_ZIP64));

  // disk numbers
  this.write(constants.LONG_ZERO);
  this.write(constants.LONG_ZERO);

  // number of entries
  this.write(zipUtil.getEightBytes(this._entries.length));
  this.write(zipUtil.getEightBytes(this._entries.length));

  // length and location of CD
  this.write(zipUtil.getEightBytes(this._archive.centralLength));
  this.write(zipUtil.getEightBytes(this._archive.centralOffset));

  // extensible data sector
  // not implemented at this time

  // end of central directory locator
  this.write(zipUtil.getLongBytes(constants.SIG_ZIP64_EOCD_LOC));

  // disk number holding the ZIP64 EOCD record
  this.write(constants.LONG_ZERO);

  // relative offset of the ZIP64 EOCD record
  this.write(zipUtil.getEightBytes(this._archive.centralOffset + this._archive.centralLength));

  // total number of disks
  this.write(zipUtil.getLongBytes(1));
};

ZipArchiveOutputStream.prototype._writeCentralFileHeader = function(ae) {
  var gpb = ae.getGeneralPurposeBit();
  var method = ae.getMethod();
  var offsets = ae._offsets;

  var size = ae.getSize();
  var compressedSize = ae.getCompressedSize();

  if (ae.isZip64() || offsets.file > constants.ZIP64_MAGIC) {
    size = constants.ZIP64_MAGIC;
    compressedSize = constants.ZIP64_MAGIC;

    ae.setVersionNeededToExtract(constants.MIN_VERSION_ZIP64);

    var extraBuf = Buffer.concat([
      zipUtil.getShortBytes(constants.ZIP64_EXTRA_ID),
      zipUtil.getShortBytes(24),
      zipUtil.getEightBytes(ae.getSize()),
      zipUtil.getEightBytes(ae.getCompressedSize()),
      zipUtil.getEightBytes(offsets.file)
    ], 28);

    ae.setExtra(extraBuf);
  }

  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_CFH));

  // version made by
  this.write(zipUtil.getShortBytes((ae.getPlatform() << 8) | constants.VERSION_MADEBY));

  // version to extract and general bit flag
  this.write(zipUtil.getShortBytes(ae.getVersionNeededToExtract()));
  this.write(gpb.encode());

  // compression method
  this.write(zipUtil.getShortBytes(method));

  // datetime
  this.write(zipUtil.getLongBytes(ae.getTimeDos()));

  // crc32 checksum
  this.write(zipUtil.getLongBytes(ae.getCrc()));

  // sizes
  this.write(zipUtil.getLongBytes(compressedSize));
  this.write(zipUtil.getLongBytes(size));

  var name = ae.getName();
  var comment = ae.getComment();
  var extra = ae.getCentralDirectoryExtra();

  if (gpb.usesUTF8ForNames()) {
    name = new Buffer(name);
    comment = new Buffer(comment);
  }

  // name length
  this.write(zipUtil.getShortBytes(name.length));

  // extra length
  this.write(zipUtil.getShortBytes(extra.length));

  // comments length
  this.write(zipUtil.getShortBytes(comment.length));

  // disk number start
  this.write(constants.SHORT_ZERO);

  // internal attributes
  this.write(zipUtil.getShortBytes(ae.getInternalAttributes()));

  // external attributes
  this.write(zipUtil.getLongBytes(ae.getExternalAttributes()));

  // relative offset of LFH
  if (offsets.file > constants.ZIP64_MAGIC) {
    this.write(zipUtil.getLongBytes(constants.ZIP64_MAGIC));
  } else {
    this.write(zipUtil.getLongBytes(offsets.file));
  }

  // name
  this.write(name);

  // extra
  this.write(extra);

  // comment
  this.write(comment);
};

ZipArchiveOutputStream.prototype._writeDataDescriptor = function(ae) {
  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_DD));

  // crc32 checksum
  this.write(zipUtil.getLongBytes(ae.getCrc()));

  // sizes
  if (ae.isZip64()) {
    this.write(zipUtil.getEightBytes(ae.getCompressedSize()));
    this.write(zipUtil.getEightBytes(ae.getSize()));
  } else {
    this.write(zipUtil.getLongBytes(ae.getCompressedSize()));
    this.write(zipUtil.getLongBytes(ae.getSize()));
  }
};

ZipArchiveOutputStream.prototype._writeLocalFileHeader = function(ae) {
  var gpb = ae.getGeneralPurposeBit();
  var method = ae.getMethod();
  var name = ae.getName();
  var extra = ae.getLocalFileDataExtra();

  if (ae.isZip64()) {
    gpb.useDataDescriptor(true);
    ae.setVersionNeededToExtract(constants.MIN_VERSION_ZIP64);
  }

  if (gpb.usesUTF8ForNames()) {
    name = new Buffer(name);
  }

  ae._offsets.file = this.offset;

  // signature
  this.write(zipUtil.getLongBytes(constants.SIG_LFH));

  // version to extract and general bit flag
  this.write(zipUtil.getShortBytes(ae.getVersionNeededToExtract()));
  this.write(gpb.encode());

  // compression method
  this.write(zipUtil.getShortBytes(method));

  // datetime
  this.write(zipUtil.getLongBytes(ae.getTimeDos()));

  ae._offsets.data = this.offset;

  // crc32 checksum and sizes
  if (gpb.usesDataDescriptor()) {
    this.write(constants.LONG_ZERO);
    this.write(constants.LONG_ZERO);
    this.write(constants.LONG_ZERO);
  } else {
    this.write(zipUtil.getLongBytes(ae.getCrc()));
    this.write(zipUtil.getLongBytes(ae.getCompressedSize()));
    this.write(zipUtil.getLongBytes(ae.getSize()));
  }

  // name length
  this.write(zipUtil.getShortBytes(name.length));

  // extra length
  this.write(zipUtil.getShortBytes(extra.length));

  // name
  this.write(name);

  // extra
  this.write(extra);

  ae._offsets.contents = this.offset;
};

ZipArchiveOutputStream.prototype.getComment = function(comment) {
  return this._archive.comment !== null ? this._archive.comment : '';
};

ZipArchiveOutputStream.prototype.isZip64 = function() {
  return this._archive.forceZip64 || this._entries.length > constants.ZIP64_MAGIC_SHORT || this._archive.centralLength > constants.ZIP64_MAGIC || this._archive.centralOffset > constants.ZIP64_MAGIC;
};

ZipArchiveOutputStream.prototype.setComment = function(comment) {
  this._archive.comment = comment;
};<|endoftext|>


<commit_before>/* Future possibilities:
 * It would be cool if this thing
 * actually emitted pieces of bdecoded
 * shit along the way. eg, the
 * torrent client could listen for the infohash
 * key, name key, and update its internal
 * data while it's still parsing the rest
 * of the file.
 *
 * This would also allow only one copy of
 * the data to be in memory, + the size of a chunk.
 */

// In order to make it not be recursive, we simply can setTimeout 0 callback our way through.
var fs = require('fs'),
    Q = require('q'),
    is = require('is-js'),
    async = require('async');

exports.bdecode = function(fileOrData, callback) {
  // Todo, stream instead of read all data at once, especially if it's a file. Allow stream as an argument
  return Q.fcall(function() {
    if(Buffer.isBuffer(fileOrData)) {
      return fileOrData.toString();
    } else if(typeof fileOrData == 'string') {
      return fileOrData;
    } else {
      // Doesn't work, async
      fs.readFile(fileOrData, function(err, res) {
        if(err) throw new Error(err);
        return res.toString();
      });
    }
  })
  .then(bdecodeData);
};

function bdecodeData(val) {
  var deferred = Q.defer();
  var out = [];
  var myNdx = 0;
  async.whilst(
    function() {
      return myNdx < val.length;
    },
    function(callback) {
      decodeElement(val, myNdx)
      .then(function(val) {
        out.push(val.obj);
        myNdx = val.ndx;
        callback(null);
      })
      .fail(callback);
    },
    function(err) {
      if(err) deferred.fail(err);
      else deferred.resolve(out);
    }
  );

  return deferred.promise;
}

function decodeInt(str, ndx) {
  var deferred = Q.defer();
  //assert(str[ndx] == 'i');
  ndx++;
  var num = 0;
  for(; str[ndx] !== 'e'; ndx++) {
    num *= 10;
    num += parseInt(str[ndx], 10);
  }
  ndx++; // 'e'
  deferred.resolve({ndx: ndx, obj: num});
  return deferred.promise;
}

function decodeStr(str, ndx) {
  var deferred = Q.defer();
  var out = '';
  var strlen = 0;
  for(; str[ndx] !== ':'; ndx++) {
    strlen *= 10;
    strlen += parseInt(str[ndx], 10);
  }
  ndx++; //bypass :
  for(var i = 0; i < strlen; i++, ndx++) {
    out += str[ndx];
  }
  deferred.resolve({ndx: ndx, obj: out});
  return deferred.promise;
}

function decodeList(str, ndx) {
  var deferred = Q.defer();
  //assert(str[ndx] === 'l')
  ndx++; // 'l'
  var list = [];
  async.whilst(
    function() {
      return str[ndx] !== 'e';
    },
    function(cb) {
      decodeElement(str, ndx)
      .then(function(res) {
        list.push(res.obj);
        ndx = res.ndx;
        cb(null);
      })
      .fail(function(err) {
        cb(err);
      });
    },
    function(err) {
      if(err) deferred.fail(err);
      else deferred.resolve({ndx: ndx + 1, obj: list});
      // +1 because of the 'e'
    }
  );
  return deferred.promise;
}

// Return format: {ndx: newNdx, obj: objDecoded}
function decodeElement(str, ndx) {
  if(str[ndx] === 'i') {
    return decodeInt(str, ndx);
  } else if(str[ndx] === 'l') {
    return decodeList(str, ndx);
  } else if(str[ndx] === 'd') {
    return decodeDict(str, ndx);
  } else {
    return decodeStr(str, ndx);
  }
}
<commit_msg>Add decodeDict, all tests pass<commit_after>/* Future possibilities:
 * It would be cool if this thing
 * actually emitted pieces of bdecoded
 * shit along the way. eg, the
 * torrent client could listen for the infohash
 * key, name key, and update its internal
 * data while it's still parsing the rest
 * of the file.
 *
 * This would also allow only one copy of
 * the data to be in memory, + the size of a chunk.
 */

// In order to make it not be recursive, we simply can setTimeout 0 callback our way through.
var fs = require('fs'),
    Q = require('q'),
    is = require('is-js'),
    async = require('async');

exports.bdecode = function(fileOrData, callback) {
  // Todo, stream instead of read all data at once, especially if it's a file. Allow stream as an argument
  return Q.fcall(function() {
    if(Buffer.isBuffer(fileOrData)) {
      return fileOrData.toString();
    } else if(typeof fileOrData == 'string') {
      return fileOrData;
    } else {
      // Doesn't work, async
      fs.readFile(fileOrData, function(err, res) {
        if(err) throw new Error(err);
        return res.toString();
      });
    }
  })
  .then(bdecodeData);
};

function bdecodeData(val) {
  var deferred = Q.defer();
  var out = [];
  var myNdx = 0;
  async.whilst(
    function() {
      return myNdx < val.length;
    },
    function(callback) {
      decodeElement(val, myNdx)
      .then(function(val) {
        out.push(val.obj);
        myNdx = val.ndx;
        callback(null);
      })
      .fail(callback);
    },
    function(err) {
      if(err) deferred.fail(err);
      else deferred.resolve(out);
    }
  );

  return deferred.promise;
}

function decodeInt(str, ndx) {
  var deferred = Q.defer();
  //assert(str[ndx] == 'i');
  ndx++;
  var num = 0;
  for(; str[ndx] !== 'e'; ndx++) {
    num *= 10;
    num += parseInt(str[ndx], 10);
  }
  ndx++; // 'e'
  deferred.resolve({ndx: ndx, obj: num});
  return deferred.promise;
}

function decodeStr(str, ndx) {
  var deferred = Q.defer();
  var out = '';
  var strlen = 0;
  for(; str[ndx] !== ':'; ndx++) {
    strlen *= 10;
    strlen += parseInt(str[ndx], 10);
  }
  ndx++; //bypass :
  for(var i = 0; i < strlen; i++, ndx++) {
    out += str[ndx];
  }
  deferred.resolve({ndx: ndx, obj: out});
  return deferred.promise;
}

function decodeList(str, ndx) {
  var deferred = Q.defer();
  //assert(str[ndx] === 'l')
  ndx++; // 'l'
  var list = [];
  async.whilst(
    function() {
      return str[ndx] !== 'e';
    },
    function(cb) {
      decodeElement(str, ndx)
      .then(function(res) {
        list.push(res.obj);
        ndx = res.ndx;
        cb(null);
      })
      .fail(function(err) {
        cb(err);
      });
    },
    function(err) {
      if(err) deferred.fail(err);
      else deferred.resolve({ndx: ndx + 1, obj: list});
      // +1 because of the 'e'
    }
  );
  return deferred.promise;
}

function decodeDict(str, ndx) {
  var deferred = Q.defer();
  //assert(str[ndx] == 'd')
  ndx++; //d
  var dict = {};
  async.whilst(
    function() {
      return str[ndx] !== 'e';
    },
    function(cb) {
      decodeElement(str, ndx)
      .then(function(keyRes) {
        decodeElement(str, keyRes.ndx)
        .then(function(valRes) {
          ndx = valRes.ndx;
          dict[keyRes.obj] = valRes.obj;
          cb(null);
        });
      });
    },
    function(err) {
      if(err) deferred.fail(err);
      else deferred.resolve({ndx: ndx + 1, obj: dict});
    }
  );

  return deferred.promise;
}

// Return format: {ndx: newNdx, obj: objDecoded}
function decodeElement(str, ndx) {
  if(str[ndx] === 'i') {
    return decodeInt(str, ndx);
  } else if(str[ndx] === 'l') {
    return decodeList(str, ndx);
  } else if(str[ndx] === 'd') {
    return decodeDict(str, ndx);
  } else {
    return decodeStr(str, ndx);
  }
}
<|endoftext|>