<|start_filename|>js/objects/tests/test-resource-with-preload.js<|end_filename|>
/**
 * Resource Part and Related Tests
 * --------------------------------------
 * These are integration tests that ensure
 * proper functioning of the Resource part, view,
 * and its various methods, functions and dependencies.
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

const responseBasic = {
    id: 1,
    name: "Basic response"
};

const responseWithPrerequisite = {
    id: 1,
    name: "Prerequisite response"
};
const test = {
    name: "TestAPI",
    load: function(url){
        this.src = url;
        return true;
    },
    response: null,
    src: null,
    get: async function(prerequisite, key){
        if(key){
            if(!this.response){
                throw Error("No result has been fetched");
            }
            return this.response[key];
        }
        if(prerequisite){
            this.response = responseWithPrerequisite;
        } else {
            this.response = responseBasic;
        }
        return JSON.stringify(this.response);
    },
};

describe('Resource', () => {
    let currentCardModel = System.getCurrentCardModel();
    let resource;
    before('', () => {
        System.availableResources["testAPI"] = test;
    });
    it('Can add resource (with name).', () => {
        let msg = {
            type: "command",
            commandName: "newModel",
            args: ["resource", currentCardModel.id, "testResource"]
        };
        currentCardModel.sendMessage(msg, currentCardModel);
        assert.equal(1, currentCardModel.subparts.length);
        resource = currentCardModel.subparts[0];
        assert.equal(resource.type, "resource");
    });
    it('Can load resource', () => {
        let msg = {
            type: "command",
            commandName: "loadResource",
            args: ["testAPI"]
        };
        resource.sendMessage(msg, resource);
        let resourceProp = resource.partProperties.getPropertyNamed(resource, "resourceName");
        assert.equal(resourceProp, "testAPI");
    });
    it('Can load resource source (url)', () => {
        let msg = {
            type: "command",
            commandName: "setSourceTo",
            args: ["someurl.com"]
        };
        resource.sendMessage(msg, resource);
        let sourceProp = resource.partProperties.getPropertyNamed(resource, "src");
        assert.equal(sourceProp, "someurl.com");
        assert.equal(test.src, "someurl.com");
    });
    it('Resource "response" and "readyState properties are null, "notReady," resp', () => {
        let responseProp = resource.partProperties.getPropertyNamed(resource, "response");
        let readyStateProp = resource.partProperties.getPropertyNamed(resource, "readyState");
        assert.isNull(responseProp);
        assert.equal(readyStateProp, "notReady");
    });
    it('Can get from resource', (done) => {
        // NOTE: the use of "done" since .get is async
        // see more here: https://mochajs.org/#asynchronous-code
        let msg = {
            type: "command",
            commandName: "get",
            args: []
        };
        let msgFunc = () => {
            resource.sendMessage(msg, resource);
            done();
        };
        expect(msgFunc).to.not.throw();
        assert.equal(responseBasic, test.response);
        let responseProp = resource.partProperties.getPropertyNamed(resource, "response");
        assert.equal(responseProp, JSON.stringify(responseBasic));
    });
    it('Resource "readyState property is "ready"', () => {
        let readyStateProp = resource.partProperties.getPropertyNamed(resource, "readyState");
        assert.equal(readyStateProp, "ready");
    });
    it('Can get with argument from resource', () => {
        let msg = {
            type: "command",
            commandName: "get",
            args: ["name"]
        };
        let msgFunc = () => {
            resource.sendMessage(msg, resource);
        };
        expect(msgFunc).to.not.throw();
    });
    it('Response property is properly set', () => {
        let responseProp = resource.partProperties.getPropertyNamed(resource, "response");
        assert.equal(responseBasic["name"], responseProp);
    });
    it('Can get from resource with prerequisite', () => {
        resource.partProperties.setPropertyNamed(resource, "prerequisite", "someInfo");
        let msg = {
            type: "command",
            commandName: "get",
            args: []
        };
        let msgFunc = () => {
            resource.sendMessage(msg, resource);
        };
        expect(msgFunc).to.not.throw();
    });
    it('Response property is properly set', () => {
        let responseProp = resource.partProperties.getPropertyNamed(resource, "response");
        assert.equal(responseProp, JSON.stringify(responseWithPrerequisite));
    });
});


<|start_filename|>js/objects/utils/id.js<|end_filename|>
// ID related utilities

import { v4 as uuidv4 } from 'uuid';

/**
 * ID Maker
 * ------------------------------------
 * I am responsible for creating globally
 * unique ID values for Parts in the SimpleTalk
 * world.
 * HC requires that all Parts have unique ids and
 * that these should not be repeated in any given
 * "application" instance.
 * We need to determine what an "application" is
 * in our context, but regardless we can use this
 * module as a drop in replacement, implementing
 * UUIDs or URLs or whatever we want.
 * For now we just increment an integer.
 */
const idMaker = {
    new: function(){
        let id = uuidv4();
        return id.replace(/-/g, '');
    }
};

/* ID checker
 * --------------------------------------
 * I am responsible for checking whether an id is
 * is valid and returning it if so
 */
const isValidId = function(id) {
    if(id === null || id === undefined || id === ""){
        return false;
    }
    if(id.length != 32 || id.match('[a-z0-9]*')[0].length != 32) {
        return false;
    }
    return id;
};

export {
    idMaker,
    isValidId,
    idMaker as default
};


<|start_filename|>js/objects/utils/icons.js<|end_filename|>
let partIcons = {};

partIcons.world = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-world" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <circle cx="12" cy="12" r="9" />
  <line x1="3.6" y1="9" x2="20.4" y2="9" />
  <line x1="3.6" y1="15" x2="20.4" y2="15" />
  <path d="M11.5 3a17 17 0 0 0 0 18" />
  <path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
`;

partIcons.stack = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-stack" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <polyline points="12 4 4 8 12 12 20 8 12 4" />
  <polyline points="4 12 12 16 20 12" />
  <polyline points="4 16 12 20 20 16" />
</svg>
`;

partIcons.card = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <rect x="4" y="4" width="16" height="16" rx="2" />
</svg>
`;

partIcons.button = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-hand-finger" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M8 13v-8.5a1.5 1.5 0 0 1 3 0v7.5" />
  <path d="M11 11.5v-2a1.5 1.5 0 1 1 3 0v2.5" />
  <path d="M14 10.5a1.5 1.5 0 0 1 3 0v1.5" />
  <path d="M17 11.5a1.5 1.5 0 0 1 3 0v4.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47" />
</svg>
`;

partIcons.window = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-window" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M12 3c-3.866 0 -7 3.272 -7 7v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1 -1v-10c0 -3.728 -3.134 -7 -7 -7z" />
  <line x1="5" y1="13" x2="19" y2="13" />
  <line x1="12" y1="3" x2="12" y2="21" />
</svg>
`;

partIcons.area = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-shape" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <circle cx="5" cy="5" r="2" />
  <circle cx="19" cy="5" r="2" />
  <circle cx="5" cy="19" r="2" />
  <circle cx="19" cy="19" r="2" />
  <line x1="5" y1="7" x2="5" y2="17" />
  <line x1="7" y1="5" x2="17" y2="5" />
  <line x1="7" y1="19" x2="17" y2="19" />
  <line x1="19" y1="7" x2="19" y2="17" />
</svg>
`;

partIcons.field = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-forms" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M12 3a3 3 0 0 0 -3 3v12a3 3 0 0 0 3 3" />
  <path d="M6 3a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3" />
  <path d="M13 7h7a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-7" />
  <path d="M5 7h-1a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h1" />
  <path d="M17 12h.01" />
  <path d="M13 12h.01" />
</svg>
`;

partIcons.drawing = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-palette" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M12 21a9 9 0 1 1 0 -18a9 8 0 0 1 9 8a4.5 4 0 0 1 -4.5 4h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25" />
  <circle cx="7.5" cy="10.5" r=".5" fill="currentColor" />
  <circle cx="12" cy="7.5" r=".5" fill="currentColor" />
  <circle cx="16.5" cy="10.5" r=".5" fill="currentColor" />
</svg>
`;

partIcons.image = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-photo" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <line x1="15" y1="8" x2="15.01" y2="8" />
  <rect x="4" y="4" width="16" height="16" rx="3" />
  <path d="M4 15l4 -4a3 5 0 0 1 3 0l5 5" />
  <path d="M14 14l1 -1a3 5 0 0 1 3 0l2 2" />
</svg>
`;

partIcons.generic = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-puzzle" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M4 7h3a1 1 0 0 0 1 -1v-1a2 2 0 0 1 4 0v1a1 1 0 0 0 1 1h3a1 1 0 0 1 1 1v3a1 1 0 0 0 1 1h1a2 2 0 0 1 0 4h-1a1 1 0 0 0 -1 1v3a1 1 0 0 1 -1 1h-3a1 1 0 0 1 -1 -1v-1a2 2 0 0 0 -4 0v1a1 1 0 0 1 -1 1h-3a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1h1a2 2 0 0 0 0 -4h-1a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1" />
</svg>
`;

export {
    partIcons,
    partIcons as default
};


<|start_filename|>js/objects/views/AreaView.js<|end_filename|>
/**
 * AreaView
 * -------------------------------
 * I am a webcomponent representation
 * of an Area, which is a grouping of
 * Parts that have some kind of layout
 * specified
 */
import PartView from './PartView.js';

const templateString = `
                <style>
                #area-wrapper {
                    display: inherit;
                    flex-direction: inherit;
                    flex-wrap: inherit;
                    align-items: inherit;
                    align-content: inherit;
                    justify-content: inherit;
                    position: relative; 
                    width: 100%;
                    height: 100%;
                    border-radius: inherit;
                }
                .clip {
                    overflow: hidden;  
                }
                .allow-scroll {
                    overflow: auto;
                }
                </style>
                <div id="area-wrapper">
                <slot></slot>
                </div>
`;

class AreaView extends PartView {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Bound methods
        this.clippingChanged = this.clippingChanged.bind(this);
        this.allowScrollingChanged = this.allowScrollingChanged.bind(this);

        // Prop change handlers
        this.onPropChange('clipping', this.clippingChanged);
        this.onPropChange('allow-scrolling', this.allowScrollingChanged);
    }

    afterModelSet(){
        let clipping = this.model.partProperties.getPropertyNamed(
            this.model,
            "clipping"
        );
        let allowScrolling = this.model.partProperties.getPropertyNamed(
            this.model,
            "allow-scrolling"
        );
        this.clippingChanged(clipping, this.model.id);
        this.allowScrollingChanged(allowScrolling, this.model.id);
    }

    clippingChanged(newVal, id){
        let wrapper = this._shadowRoot.getElementById('area-wrapper');
        if(newVal == true){
            wrapper.classList.remove('allow-scroll');
            wrapper.classList.add('clip');
        } else {
            wrapper.classList.remove('clip');
        }
    }

    allowScrollingChanged(newVal, id){
        let wrapper = this._shadowRoot.getElementById('area-wrapper');
        if(newVal == true){
            wrapper.classList.remove('clip');
            wrapper.classList.add('allow-scroll');
            // this.classList.add('outer-allow-scroll');
        } else {
            // this.classList.remove('outer-allow-scroll');
            wrapper.classList.remove('allow-scroll');
        }
    }

    addContextMenuItems(contextMenu){
        contextMenu.addSpacer();
        let layout = this.model.partProperties.getPropertyNamed(
            this.model,
            'layout'
        );
        let direction = this.model.partProperties.getPropertyNamed(
            this.model,
            'list-direction'
        );
        if(layout != 'list'){
            contextMenu.addListItem(
                "Set Layout to List",
                (event) => {
                    this.model.partProperties.setPropertyNamed(
                        this.model,
                        'layout',
                        'list'
                    );
                }
            );
        } else {
            contextMenu.addListItem(
                "Set Layout to Strict",
                (event) => {
                    this.model.partProperties.setPropertyNamed(
                        this.model,
                        'layout',
                        'strict'
                    );
                }
            );
            if(direction == 'row'){
                contextMenu.addListItem(
                    "Set List Direction to Column",
                    (event) => {
                        this.model.partProperties.setPropertyNamed(
                            this.model,
                            'list-direction',
                            'column'
                        );
                    }
                );
            } else {
                contextMenu.addListItem(
                    "Set List Direction to Row",
                    (event) => {
                        this.model.partProperties.setPropertyNamed(
                            this.model,
                            'list-direction',
                            'row'
                        );
                    }
                );
            }
        }
    }
};

export {
    AreaView,
    AreaView as default
};


<|start_filename|>js/objects/views/editors/EditorPropItem.js<|end_filename|>
// PREAMBLE

const checkIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-circle-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#00b341" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <circle cx="12" cy="12" r="9" />
  <path d="M9 12l2 2l4 -4" />
</svg>
`;

const cancelIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-circle-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#ff2825" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <circle cx="12" cy="12" r="9" />
  <path d="M10 10l4 4m0 -4l-4 4" />
</svg>
`;

const templateString = `
<style>
    li {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 6px;
        padding-left: 8px;
        padding-right: 8px;
        margin-top: 6px;
    }
   
    li > label {
        flex: 1;
    }

    :host {
        width: 100%;
    }

    :host(.item-hidden) {
        display:none;
    }

    button {
        outline: none;
        border: 1px solid transparent;
        background-color: transparent;
        opacity: 1.0;
        transition: opacity 0.1s linear;
    }

    button:disabled {
        opacity: 0.05;
        transition: opacity: 0.1s linear;
    }

    button:hover {
        cursor: pointer;
    }

    button.button-hidden {
        display: none;
    }

    label {
        font-family: monospace;
    }

    input {
        border: 1px solid transparent;
        border-bottom: 1px solid rgba(100, 100, 100, 0.5);
        outline: none;
    }
    
    input:focus {
        border: 1px solid rgba(100, 100, 100, 0.8);
    }
</style>
<li>
    <label for="prop-value"></label>
    <input id="prop-value" name="prop-value"/>
    <button id="accept" disabled>${checkIcon}</button>
    <button id="cancel" disabled>${cancelIcon}</button>
</li>
`;

class EditorPropItem extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // By default, there is no property
        this.property = null;
        this.owner = null;

        // Bound methods
        this.render = this.render.bind(this);
        this.onInputChange = this.onInputChange.bind(this);
        this.onInputInput = this.onInputInput.bind(this);
        this.onAcceptClick = this.onAcceptClick.bind(this);
        this.onCancelClick = this.onCancelClick.bind(this);
        this.onEnterKey = this.onEnterKey.bind(this);
        this.setupNumericInput = this.setupNumericInput.bind(this);
        this.enableButtons = this.enableButtons.bind(this);
        this.disableButtons = this.disableButtons.bind(this);
    }

    setProperty(aProperty, anOwner){
        // Remove any existing event handlers
        // TODO
        this.property = aProperty;
        this.owner = anOwner;

        // Update the element attributes
        this.setAttribute('name', this.property.name);
        this.setAttribute('owner-id', this.owner.id);

        // Add new event handlers

        // Render
        this.render();
        
    }

    connectedCallback(){
        if(this.isConnected){
            this.addEventListener('keypress', this.onEnterKey);
        }
    }

    disconnectedCallback(){
        this.removeEventListener('keypress', this.onEnterKey);
    }

    render(){
        this.labelElement = this._shadowRoot.querySelector('label');
        this.inputElement = this._shadowRoot.querySelector('input');
        this.acceptButton = this._shadowRoot.getElementById('accept');
        this.cancelButton = this._shadowRoot.getElementById('cancel');

        // Remove any hide classes
        this.acceptButton.classList.remove('button-hidden');
        this.cancelButton.classList.remove('button-hidden');
        
        // Remove any bound events
        this.inputElement.removeEventListener('input', this.onInputInput);
        this.inputElement.removeEventListener('change', this.onInputChange);
        this.acceptButton.removeEventListener('click', this.onAcceptClick);
        this.cancelButton.removeEventListener('click', this.onCancelClick);

        // Add new events
        this.inputElement.addEventListener('input', this.onInputInput);
        this.inputElement.addEventListener('change', this.onInputChange);
        this.acceptButton.addEventListener('click', this.onAcceptClick);
        this.cancelButton.addEventListener('click', this.onCancelClick);
        
        this.labelElement.textContent = `${this.property.name}:`;
        let currentVal = this.property.getValue(this.owner);
        if(currentVal == null || currentVal == undefined){
            // Do something different here
        } else if(typeof(currentVal) == 'number'){
            this.setupNumericInput();
        } else if(typeof(currentVal) == 'boolean'){
            this.inputElement.setAttribute('type', 'checkbox');
            this.inputElement.checked = currentVal;
            this.acceptButton.classList.add('button-hidden');
            this.cancelButton.classList.add('button-hidden');
        } else {
            this.inputElement.setAttribute('type', 'text');
        }

        this.inputElement.setAttribute('placeholder', currentVal);
        this.inputElement.value = currentVal;
    }

    setupNumericInput(){
        if(this.property.name.endsWith('-transparency')){
            this.inputElement.setAttribute('type', 'range');
            this.inputElement.setAttribute('step', '0.05');
            this.inputElement.setAttribute('min', '0.0');
            this.inputElement.setAttribute('max', '1.0');
        } else {
            this.inputElement.setAttribute('type', 'number');
        }
    }

    onInputChange(event){
        if(event.target.type == "checkbox"){
            this.owner.partProperties.setPropertyNamed(
                this.owner,
                this.property.name,
                event.target.checked
            );
        }
    }

    onInputInput(event){
        let inputType = event.target.getAttribute('type');
        if(inputType == 'range'){
            return this.onAcceptClick();
        }
        if(event.target.value !== this.property.getValue(this.owner)){
            this.enableButtons();
        } else {
            this.disableButtons();
        }
    }

    onEnterKey(event){
        if(event.key == 'Enter'){
            this.onAcceptClick();
        }
    }

    enableButtons(){
        this.acceptButton.removeAttribute('disabled');
        this.cancelButton.removeAttribute('disabled');
    }

    disableButtons(){
        this.acceptButton.setAttribute('disabled', true);
        this.cancelButton.setAttribute('disabled', true);
    }

    onAcceptClick(event){
        let value = this.inputElement.value;
        if(this.inputElement.type == 'number'){
            value = parseFloat(value);
            if(isNaN(value)){
                // if we can't parse the value just let it go through
                // as it might be a prop style keyword such as "fill"
                // TODO: we might want to limit this to a set of prop keywords
                value = this.inputElement.value;
            }
        } else if(this.inputElement.type == 'range'){
            value = parseFloat(value);
        } else if(this.inputElement.type == 'checkbox'){
            value = this.inputElement.checked;
        }
        this.owner.partProperties.setPropertyNamed(
            this.owner,
            this.property.name,
            value
        );
        this.disableButtons();
    }

    onCancelClick(event){
        this.inputElement.value = this.owner.partProperties.getPropertyNamed(
            this.owner,
            this.property.name
        );
        this.disableButtons();
    }
};

export {
    EditorPropItem,
    EditorPropItem as default
};


<|start_filename|>js/objects/views/FieldView.js<|end_filename|>
/**
 * FieldView
 * ---------------------------------
 * I am the view of an Field part.
 * I am an "interim" view intended to display
 * and edit plain text on a Card.
 * I should be replaced with a more comprehensive
 * implementation of Field/FieldView in the future.
 */
import PartView from './PartView.js';
import cssStyler from '../utils//styler.js';
import ColorWheelWidget from './drawing/ColorWheelWidget.js';
import interpreterSemantics from '../../ohm/interpreter-semantics.js';
import createHighlighter from '../utils/AltSyntaxHighlighter.js';

const haloEditButtonSVG = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tools" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M3 21h4l13 -13a1.5 1.5 0 0 0 -4 -4l-13 13v4" />
  <line x1="14.5" y1="5.5" x2="18.5" y2="9.5" />
  <polyline points="12 8 7 3 3 7 8 12" />
  <line x1="7" y1="8" x2="5.5" y2="9.5" />
  <polyline points="16 12 21 17 17 21 12 16" />
  <line x1="16" y1="17" x2="14.5" y2="18.5" />
</svg>
`;

const haloLockButtonSVG = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
   <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
   <rect x="5" y="11" width="14" height="10" rx="2"></rect>
   <circle cx="12" cy="16" r="1"></circle>
   <path d="M8 11v-4a4 4 0 0 1 8 0v4"></path>
</svg>
`;

const haloUnlockButtonSVG = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock-open" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
   <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
   <rect x="5" y="11" width="14" height="10" rx="2"></rect>
   <circle cx="12" cy="16" r="1"></circle>
   <path d="M8 11v-5a4 4 0 0 1 8 0"></path>
</svg>
`;

const fieldTemplateString = `
      <style>
        .field {
            display: flex;
            align-items: center;
            flex-direction: column;
            height: 100%;
            width: 100%;
            overflow: auto;
        }

        .field color-wheel {
            position: absolute;
        }

        .field-textarea {
            width: calc(100% - 5px);
            height: 100%;
            width: 100%;
            white-space: pre-wrap;
            overflow-wrap: anywhere;
        }

        /* Syntax Highlighting
 *---------------------------------------------------*/
span[data-st-rule="messageName"]{
    text-decoration: underline;
}

span[data-st-rule="keyword"]{
    font-weight: bold;
}

span[data-st-rule="ParameterList-item"]{
    font-style: italic;
    color: grey;
}


    </style>
    <div class="field">
        <div class="field-textarea" spellcheck="false"></div>
    </div>`;


function formatDoc(sCmd, sValue) {
  document.execCommand(sCmd, false, sValue); oDoc.focus();
}

class FieldView extends PartView {
    constructor(){
        super();

        // this.editorCompleter = this.simpleTalkCompleter;
        this.textStyler = cssStyler;  // we might want to consider a more programmatic way to set this
        this.editorCompleter = null;
        this.contextMenuOpen = false;
        this.haloLockUnlockButton = null;
        this.selectionRanges = {};
        this.wantsContextMenu = false;

        // Presets for syntax highlighting.
        // When highlighting is enabled, we will check
        // each line of the text for the following
        // grammatical rules:
        this._syntaxRules = [
            "MessageHandlerOpen",
            "MessageHandlerClose"
        ];

        this.template = document.createElement('template');
        this.template.innerHTML = fieldTemplateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            this.template.content.cloneNode(true)
        );

        // Bind methods
        this.onInput = this.onInput.bind(this);
        this.onBeforeInput = this.onBeforeInput.bind(this);
        this.onClick = this.onClick.bind(this);
        this.onKeydown = this.onKeydown.bind(this);
        this.onMousedown = this.onMousedown.bind(this);
        this.openContextMenu = this.openContextMenu.bind(this);
        this.closeContextMenu = this.closeContextMenu.bind(this);
        this.doIt = this.doIt.bind(this);
        this.handleSelection = this.handleSelection.bind(this);
        this.openField = this.openField.bind(this);
        this.textToHtml = this.textToHtml.bind(this);
        this.setupPropHandlers = this.setupPropHandlers.bind(this);
        this.simpleTalkCompleter = this.simpleTalkCompleter.bind(this);
        this.initCustomHaloButtons = this.initCustomHaloButtons.bind(this);
        this.insertRange = this.insertRange.bind(this);
        this.setRangeInTarget = this.setRangeInTarget.bind(this);
        this.setSelection = this.setSelection.bind(this);
        this.highlightSyntax = this.highlightSyntax.bind(this);
        this.unhighlightSyntax = this.unhighlightSyntax.bind(this);

        this.setupPropHandlers();
    }

    setupPropHandlers(){
        this.onPropChange('editable', (value, id) => {
            this.textarea.setAttribute('contenteditable', value);
            if(value === true){
                this.haloLockUnlockButton = this.haloLockButton;
                this.classList.add("editable");
            } else if (value === false){
                this.haloLockUnlockButton = this.haloUnlockButton;
                this.classList.remove("editable");
            }
        });
        // 'text' is a DynamicProp whose setter will set the corresponding
        // value for `innerHTML`. This way we can have programmatic content
        // setting and still allow to not loose markup.
        // 'innerHTML' is a BasicProp. See how these are set, without
        // notification in this.onInput()
        this.onPropChange('innerHTML', (value, id) => {
            this.textarea.innerHTML = value;
            this.model.partProperties.setPropertyNamed(
                this.model,
                "text",
                this.textarea.innerText,
                false // do not notify, to avoid an infinite loop
            );
        });
    }

    afterConnected(){
        // The events here are added via the .addEventListener() API which is
        // distinct from the this.eventRespond() which uses the DOM element
        // element.onEvent API. This allows us to distnguish between "core"
        // system-web events that we don't want meddled with at the moment, like
        // entering text in a field, and ones exposed in the environemnt for scripting
        this.textarea = this._shadowRoot.querySelector('.field-textarea');

        this.textarea.addEventListener('input', this.onInput);
        //this.textarea.addEventListener('beforeinput', this.onBeforeInput);
        this.textarea.addEventListener('keydown', this.onKeydown);
        this.textarea.addEventListener('mousedown', this.onMousedown);
        // No need to add a click listener as the base PartView class does that

        // in order to deal with range insertions (for styling text fragments within
        // the textarea), we need to have the default paragraph tag = </br>. Otherwise
        // the insert new line is of the form <div></br><div> which causes the appearance
        // of newlines when nodes are inserted into a range
        //document.execCommand("defaultParagraphSeparator", false, "st-line");
    }

    afterDisconnected(){
        this.textarea.removeEventListener('input', this.onInput);
        //this.textarea.removeEventListener('beforeinput', this.onBeforeInput);
        this.textarea.removeEventListener('keydown', this.onKeydown);
        this.textarea.removeEventListener('mousedown', this.onMousedown);
    }

    afterModelSet(){
        this.textarea = this._shadowRoot.querySelector('.field-textarea');
        // If we have a model, set the value of the textarea
        // to the current html of the field model
        let innerHTML = this.model.partProperties.getPropertyNamed(
            this.model,
            'innerHTML'
        );
        this.textarea.innerHTML = innerHTML;

        let isEditable = this.model.partProperties.getPropertyNamed(this.model, "editable");
        this.textarea.setAttribute('contenteditable', isEditable);

        // setup the lock/unlock halo button
        this.initCustomHaloButtons();
        let editable = this.model.partProperties.getPropertyNamed(
            this.model,
            'editable'
        );
        if(editable === true){
            this.haloLockUnlockButton = this.haloLockButton;
            this.classList.add("editable");
        } else if (editable === false){
            this.haloLockUnlockButton = this.haloUnlockButton;
            this.classList.remove("editable");
        }
    }

    simpleTalkCompleter(element){
        let textContent = this.htmlToText(element);
        let startOfHandlerRegex = /^on\s(\w+)(\s|\n)+$/;
        let match = textContent.match(startOfHandlerRegex);
        if(match){
            let messageName = match[1];
            // if input break is a new line then an extra
            // <div></br></div> has beed added into the elemen already
            let tabLine = "\t\n";
            if(match[2] === "\n"){
                tabLine= "";
            }
            textContent = `${tabLine}end ${messageName}`;
            let innerHTML = this.textToHtml(textContent);
            element.insertAdjacentHTML("beforeend", innerHTML);
        }
        return element.innerHTML;
    }

    /*
     * I override my base-class's implementation to handle target related functionality
     */
    styleTextCSS(){
        let textarea = this._shadowRoot.querySelector('.field-textarea');
        let cssStyle = this.model.partProperties.getPropertyNamed(this, "cssTextStyle");
        Object.keys(cssStyle).forEach((key) => {
            let value = cssStyle[key];
            textarea.style[key] = value;
        });
        // if there is a target and range set then send the target an update message
        let target = this.model.partProperties.getPropertyNamed(this.model, 'target');
        if(target){
            this.setRangeInTarget(target, this.textarea.innerHTML, cssStyle);
        }
    }

    /*
     * set a text-* property on selection to style the selected text
     * Note: this is done for every current selection, i.e. everthing
     * in this.selectionRanges
     */
    setSelection(propName, value){
        Object.values(this.selectionRanges).forEach((range) => {
            let currentStyle = {};
            // // if the document fragment has one child node and it's a span
            // // we should style that directly. This avoids unncessary DOM elements
            // // being created to wrap the contents, such as when styling is continually
            // // applied ot the same selection
            // let span;
            // if(docFragment.childNodes.length == 1 && docFragment.childNodes[0].nodeName == "SPAN"){
            //     span = docFragment.childNodes[0];
            //     // Note the use of Obejct.values here for the DOM style attribute object
            //     // that's weird
            //     Object.values(span.style).forEach((key) => {
            //         currentStyle[key] = span.style[key];
            //     });
            // } else {
            //     // we need to create a span element to wrap the contents in style
            //     span = document.createElement('span');
            //     // While tempting to use range.surroundContents() avoid this
            //     // since it will fail with a non-informative error if the range
            //     // includes partial nodes (ex text across various nodes)
            //     while (docFragment.childNodes.length){
            //         span.appendChild(docFragment.childNodes[0]);
            //     }
            // }
            let span = document.createElement('span');
            let cssObject = this.textStyler(currentStyle, propName, value);
            Object.keys(cssObject).forEach((key) => {
                span.style[key] = cssObject[key];
            });

            span.append(range.extractContents());
            range.insertNode(span);
            
            this.model.partProperties.setPropertyNamed(
                this.model,
                'innerHTML',
                this.textarea.innerHTML,
                false // do not notify
            );
            // if there is a target and range set then send the target an update message
            let target = this.model.partProperties.getPropertyNamed(this.model, 'target');
            if(target){
                this.setRangeInTarget(target, this.textarea.innerHTML);
            }
        });
    }

    onBeforeInput(event){
        let selection = document.getSelection();
        let selectedRange = selection.getRangeAt(0);
        let range = selectedRange.cloneRange();

        let innerHTML = event.target.innerHTML;
        if(!innerHTML.endsWith("<div><br></div>")){
            innerHTML += "<div><br></div>";
            event.target.innerHTML = innerHTML;
        }
        
        if(event.inputType == "insertParagraph"){
            //event.stopPropagation();
            event.preventDefault();

            let br = document.createElement('br');
            let br2 = document.createElement('br');
            range.insertNode(br);
            range.collapse(false);
            range.insertNode(br2);
            range.setStartAfter(br2);
            range.setEndAfter(br2);
        }
    }

    onInput(event){
        let innerHTML = event.target.innerHTML;
        /*
        if(!innerHTML.endsWith("<br>")){
            innerHTML += "<br>";
            event.target.innerHTML = innerHTML;
        }
        */

        if(this.editorCompleter){
            // TODO sort out how this would work
            let innerHTML = event.target.innerHTML;
            innerHTML = this.editorCompleter(event.target);
        }

        this.model.partProperties.setPropertyNamed(
            this.model,
            'text',
            event.target.innerText,
            false // do not notify, to preserve contenteditable context
        );
        this.model.partProperties.setPropertyNamed(
            this.model,
            'innerHTML',
            event.target.innerHTML,
            false // do not notify
        );
        // Since we update the 'text' property without notification, the part/model
        // is not sent the "propertyChanged" message so we do so manually
        this.model.propertyChanged("text", event.target.innerText);
        // if there is a target and range set then send the target an update message
        let target = this.model.partProperties.getPropertyNamed(this.model, 'target');
        if(target){
            this.setRangeInTarget(target, event.target.innerHTML);
        }
    }

    onKeydown(event){
        // prevent the default tab key to leave focus on the field
        if(event.key==="Tab"){
            event.preventDefault();
            //document.execCommand('insertHTML', false, '&#x9');
            let sel = document.getSelection();
            let range = sel.getRangeAt(0);

            let tabNodeValue = '\t';
            let tabNode = document.createTextNode(tabNodeValue);

            range.insertNode(tabNode);

            range.setStartAfter(tabNode);
            range.setEndAfter(tabNode);
        };
    }

    onMousedown(event){
        // clear all selections
        this.selectionRanges = {};
    }
    onClick(event){
        event.preventDefault();
        event.stopPropagation();
        if(event.button == 0){
            // if the shift key is pressed we toggle the halo
            if(event.shiftKey){
                this.onHaloActivationClick(event);
            } else{
                let text = window.getSelection().toString();
                // if no text is selected we do nothing
                if(text){
                    // if the altKey is pressed we open the context ("do it") menu
                    if(event.altKey){
                        if(!this.contextMenuOpen){
                            this.openContextMenu();
                        }
                    } else {
                        this.handleSelection(event.metaKey);
                    }
                } else {
                    // make sure no context menu is open
                    if(this.contextMenuOpen){
                        this.closeContextMenu();
                    }
                    // clear all the selections
                    this.selectionRanges = {};
                }
            }
        }
    }

    /* I handle selected text, creating a new field model/view
     * for every range in the selection, keeping track of every range
     * in this.selection Object/dict so that modification can be inserted
     * back into the corresponding ranges.
     */
    handleSelection(openNewField){
        let selection = window.getSelection();
        for(let i=0; i < selection.rangeCount; i++){
            // make sure this is not a continuing selection
            // and that the range is not already registered
            let range = selection.getRangeAt(i);
            let currentRanges = Object.values(this.selectionRanges);
            if(currentRanges.indexOf(range) >= 0){
                continue;
            }
            // we generate our own range ids, since we want this to correspond to
            // selection order which is not respected by the browser selection object
            // to ensure we don't hit on other views' ranges by accident we need unique id's
            let rangeId = Date.now(); //TODO we need a better random id
            this.selectionRanges[rangeId] = range;
            if(openNewField){
                // open a field for each new selection and populate it with the range html
                this.openField(range, rangeId);
            }
        }
    }

    openField(range, rangeId){
        // create an HTML document fragment from the range to avoid dealing wiht start/end
        // and offset calculations
        // fragments don't have the full html DOM element API so we need to create one
        let span = document.createElement('span');
        span.appendChild(range.cloneContents());

        // TODO these should all be messages and correspnding command handler definitions
        // should be part of the field's own script
        let fieldModel = window.System.newModel("field", this.model._owner.id, `selection ${rangeId}`);
        fieldModel.partProperties.setPropertyNamed(fieldModel, "innerHTML", span.innerHTML);
        fieldModel.partProperties.setPropertyNamed(fieldModel, "target", `field id ${this.model.id}`);
        fieldModel.partProperties.setPropertyNamed(fieldModel, "targetRangeId", rangeId);
    }

    /**
      * Given a tagrget specifier and html
      * I first look up to make sure that the target has the corresponding
      * range (coming from the targetRangeId property), and then set it with my
      * innerHTML. Note, since the target property value is an object specifier I
      * create a semantics objects and interpret the value resulting in a valid
      * part id.
      */
    setRangeInTarget(targetSpecifier, html, css){
        let targetRangeId = this.model.partProperties.getPropertyNamed(this.model, 'targetRangeId');
        let match = window.System.grammar.match(targetSpecifier, "ObjectSpecifier");
        let semantics = window.System.grammar.createSemantics();
        semantics.addOperation('interpret', interpreterSemantics(this.model, window.System));
        let targetId = semantics(match).interpret();

        this.model.sendMessage({
            type: "command",
            commandName: "insertRange",
            args: [targetRangeId, html, css]
        }, window.System.partsById[targetId]);
    }

    /*
     * I insert the html (string) into the specified range (by id)
     */
    insertRange(rangeId, html, cssObj){
        let range = this.selectionRanges[rangeId];
        if(range){
            let span = document.createElement('span');
            span.innerHTML = html;
            if(cssObj){
                Object.keys(cssObj).forEach((key) => {
                    let value = cssObj[key];
                    span.style[key] = value;
                });
            }
            range.deleteContents();
            range.insertNode(span);
            // update the text and innerHTML properties without notification
            // to prevent unnecessary setting of the text/html
            this.model.partProperties.setPropertyNamed(
                this.model,
                'text',
                this.textarea.innerText,
                false // do not notify, to preserve contenteditable context
            );
            this.model.partProperties.setPropertyNamed(
                this.model,
                'innerHTML',
                this.textarea.innerHTML,
                false // do not notify
            );
        }
    }

    openContextMenu(){
        let text = document.getSelection().toString();
        let focusNode = document.getSelection().focusNode;
        let button = document.createElement("button");
        button.id = "doIt";
        button.style.marginLeft = "10px";
        button.style.backgroundColor = "var(--palette-green)";
        button.textContent = "Do it!";
        button.addEventListener("click", this.doIt);
        focusNode.after(button);
        this.contextMenuOpen = true;
    };

    closeContextMenu(){
        let button = this._shadowRoot.querySelector('#doIt');
        if(button){
            button.remove();
        }
        // clear the selection and set the context menu to closed
        document.getSelection().removeAllRanges();
        this.contextMenuOpen = false;
    }

    doIt(event){
        event.stopPropagation();
        let text = document.getSelection().toString();
        // clean up the text to make sure no newlines or spaces made it in
        text = text.replace(/^[\t\n ]+/, "");
        text = text.replace(/[\t\n ]+$/, "");
        this.closeContextMenu();
        // send message to compile the prepped script
        let script = `on doIt\n   ${text}\nend doIt`;
        // send these messages from the model (not the view)
        // since if there is an error the original sender will
        // have an id
        this.model.sendMessage(
            {
                type: "compile",
                codeString: script,
                targetId: this.model.id
            },
            this.model
        );
        this.model.sendMessage(
            {
                type: "command",
                commandName: "doIt",
                args: [],
                shouldIgnore: true // Should ignore if System DNU
            },
            this.model
        );
    }

    initCustomHaloButtons(){
        this.haloLockButton = document.createElement('div');
        this.haloLockButton.id = "halo-field-lock-editor";
        this.haloLockButton.classList.add('halo-button');
        this.haloLockButton.innerHTML = haloLockButtonSVG;
        this.haloLockButton.style.marginRight = "6px";
        this.haloLockButton.setAttribute('slot', 'bottom-row');
        this.haloLockButton.setAttribute('title', 'Lock Editing');
        this.haloLockButton.addEventListener('click', () => {
            this.model.sendMessage({
                type: 'command',
                commandName: 'setProperty',
                args: ["editable", false],
            }, this.model);
            this.closeHalo();
            this.openHalo();
            // close/open the halo to update the editing state toggle button
        });
        this.haloUnlockButton = document.createElement('div');
        this.haloUnlockButton.id = "halo-field-unlock-editor";
        this.haloUnlockButton.classList.add('halo-button');
        this.haloUnlockButton.innerHTML = haloUnlockButtonSVG;
        this.haloUnlockButton.style.marginRight = "6px";
        this.haloUnlockButton.setAttribute('slot', 'bottom-row');
        this.haloUnlockButton.setAttribute('title', 'Unlock Editing');
        this.haloUnlockButton.addEventListener('click', () => {
            this.model.sendMessage({
                type: 'command',
                commandName: 'setProperty',
                args: ["editable", true],
            }, this.model);
            // close/open the halo to update the editing state toogle button
            this.closeHalo();
            this.openHalo();
        });
    }

    openHalo(){
        // Override default. Here we add a custom button
        // when showing.
        let foundHalo = this.shadowRoot.querySelector('st-halo');
        if(!foundHalo){
            foundHalo = document.createElement('st-halo');
            this.shadowRoot.appendChild(foundHalo);
        }
        foundHalo.append(this.haloLockUnlockButton);
    }

    // Overwriting the base class open/close editor methods
    openEditor(){
        window.System.openEditorForPart(this.model.id);
    }

    closeEditor(){
        window.System.closeEditorForPart(this.model.id);
    }


    /*
     * I convert raw text to html respecting the Firefox
     * contenteditable attribute guidelnes.
     * This means that single lins of text are left as is;
     * multiline text, i.e. text which includes "\n", is
     * wrapped in <div></div> for every line; and the last
     * line gets a <br> tag inserted before the </div> to reflect
     * the "on-enter-key" behavior.
     */
    textToHtml(text){
        if(text){
            let textLines = text.split("\n");
            if(textLines.length > 1){
                let html = "";
                textLines.forEach((line) => {
                    if(line){
                        html += `<div>${line}</div>`;
                    } else {
                        html += "<div><br></div>";
                    }
                });
                return  `<div>${html}<br></div>`;
            } else {
                return text;
            }
        } else {
            return "";
        }
    }

    htmlToText(element){
        // TODO this is very naive and ignores most possible structure
        if(element.innerHTML){
            // first replace all the "</div><div>" with line breaks
            let cleanHTML =  element.innerHTML.replace(/<\/div><div>/g, "\n");
            // then remove all html
            let tempElement = document.createElement("div");
            tempElement.innerHTML = cleanHTML;
            let cleanText = tempElement.textContent;
            tempElement.remove();
            return cleanText;
        } else {
            return "";
        }
    }

    highlightSyntax(){
        let current = this.getAttribute("syntax");
        if(current && current !== "false"){
            this.unhighlightSyntax();
        }
        let semantics = window.System.grammar.createSemantics();
        semantics.addOperation(
            "highlightSyntax",
            createHighlighter(this)
        );
        let text = this.model.partProperties.getPropertyNamed(
            this.model,
            "text"
        );
        if(!text){
            return;
        }
        let newHTML = text.split("\n").map(line => {
            // Loop through each rule and try to match
            for(let i = 0; i < this._syntaxRules.length; i++){
                let rule = this._syntaxRules[i];
                let match = window.System.grammar.match(line, rule);
                if(match.succeeded()){
                    return semantics(match).highlightSyntax().outerHTML;
                }
            }
            return line;
        }).join("\n");

        this.model.partProperties.setPropertyNamed(
            this.model,
            "innerHTML",
            newHTML
        );

        this.toggleAttribute("syntax", true);
    }

    unhighlightSyntax(){
        let text = this.model.partProperties.getPropertyNamed(
            this.model,
            "text"
        );
        if(!text){
            return;
        }
        let plainElements = text.split("\n").map(line => {
            let div = document.createElement("div");
            div.innerText = line;
            return div;
        });
        let finalLine = document.createElement("div");
        finalLine.append(document.createElement("br"));
        plainElements.push(finalLine);

        let newHTML = "";
        plainElements.forEach(element => {
            newHTML += element.outerHTML;
        });

        this.model.partProperties.setPropertyNamed(
            this.model,
            "innerHTML",
            newHTML
        );

        this.toggleAttribute("syntax", false);
    }
};

export {
    FieldView,
    FieldView as default
};


<|start_filename|>developer/devtools/panels/panel.css<|end_filename|>
body {
    width: 100vw;
    height: 100vh;
    padding: 0;
    margin: 0;
};

table {
    width: 100%!important;
    text-align: center;
}

thead {
    background-color: #EFEFEF;
}

tbody > :nth-child(even) {
    background-color: #EFEFEF;
}

tr {
    border-bottom: 1px solid black;
}

tr > * {
    width: 30vw;
    border-right: 1px solid black;
    cursor: pointer;
}



<|start_filename|>js/ohm/tests/test-comment-grammar.js<|end_filename|>
/**
 * Test Comments in the grammar
 */

ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
}
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
}
const matchAndsemanticMatchTest = (str, semanticType) => {
    matchTest(str);
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
}
const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
}
String.prototype.unCapitalize = function() {
    return this.charAt(0).toLowerCase() + this.slice(1);
}

describe("Comment Grammar Tests", () => {
    describe("Basic", () => {
        it("Can parse a basic comment", () => {
            let str = "-- this is a comment";
            semanticMatchTest(str, "comment");
        });
        it("Can parse a comment with special characters", () => {
            let str = "-- !@#$%^&%& -- -^^@%#";
            semanticMatchTest(str, "comment");
        });
        it("Can parse an empty comment", () => {
            let str = "--";
            semanticMatchTest(str, "comment");
        });
    });
    describe("Full line comments", () => {
        it("Can parse a single line comment", () => {
            let str = "-- this is a comment that takes up a whole line\n";
            semanticMatchTest(str, "StatementLine");
        });
        it("Can match a block of comment lines", () => {
            let str = [
                '-- this is the first line of the comment block',
                '-- this is the second line of the comment block',
                '--this is the last line of the -- comment block\n'
            ].join("\n");
            semanticMatchTest(str, "CommentLines");
        });
    });
    describe("Inline comments", () => {
        it("Can handle a comment that comes after a command (parses Statement)", () => {
            let str = "put 5 into myVar -- the variable we will use";
            semanticMatchTest(str, "Statement");
        });
    });

    describe("Comprehensive Tests", () => {
        it("Can handle comments between message handlers", () => {
            let str = [
                'on myMessageOne',
                '\tdoSomething',
                'end myMessageOne',
                '\n',
                '-- This is a block of comment',
                '-- between handlers#$%$',
                '\n',
                'on doSomething',
                'answer "hello"',
                'end doSomething'
            ].join('\n');
            matchTest(str, 'Script');
        });

        it("Can handle comments at the inside top of a handler", () => {
            let str = [
                'on myCustomHandler foo1, foo2',
                '-- this is a comment that documents the',
                '-- handler @#$$&^',
                'doSomethingElse',
                'end myCustomHandler'
            ].join("\n");
            semanticMatchTest(str, 'MessageHandler');
        });
    });
});


<|start_filename|>developer/devtools/content-script.js<|end_filename|>
/*
 * I am the conect script which is injected into the target
 * document window when devtools is open.
 *
 * I create connection port to handle communication between myself
 * and the devtools browser script (which then passes these messages
 * onto the devtools panel scripts).
 *
 * In addition, I handle incoming window level messaging (
 * window.postMessage() API) and routing these application
 * originating messaged to the devtools background.
 */

var portFromCS = browser.runtime.connect({name:"port-from-cs"});

// at the moment nothing much is done with messages going
// to the content-script port
portFromCS.onMessage.addListener(function(msg) {
    console.log("recieved message from background", msg);
});

window.addEventListener("message", (event) => {
    // filter on the target windows url
    if(event.origin === window.location.origin){
        // reoute the message to the background script
        portFromCS.postMessage({data: event.data});
    }
}, false);


<|start_filename|>js/ohm/tests/test-thereisa-grammar.js<|end_filename|>
/**
 * Tests for the 'there is a'command(s)
 */
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
};
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};
const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
};

// Available system objects
const objects = [
    "background", "card", "area", "field", "button", "stack", "window",
    "drawing", "audio", "image"
];

describe("'there is a' ObjectSpecifier" , () => {
    it ("Basic (current card)", () => {
        const s = `there is a current card`;
        semanticMatchTest(s, "Conditional");
        semanticMatchTest(s, "ThereIsAnObjectConditional");
    });
    it ("Basic (wth id)", () => {
        objects.forEach((d) => {
            const s = `there is a ${d} 20`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsAnObjectConditional");
        });
    });
    it ("By name of current stack", () => {
        objects.forEach((d) => {
            const s = `there is a ${d} "New" of current stack`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsAnObjectConditional");
        });
    });
    it ("Basic (with name, wth id)", () => {
        objects.forEach((d) => {
            const s = `there is a ${d} "newPart 123" of card 20`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsAnObjectConditional");
        });
    });
    it ("'Of this'", () => {
        objects.forEach((d) => {
            const s = `there is a  ${d} "newPart 123" of this stack`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsAnObjectConditional");
        });
    });
    it ("Named of 'current'", () => {
        objects.forEach((d) => {
            const s = `there is a ${d} "newPart123" of current stack`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsAnObjectConditional");
        });
    });
    it("no target or context (should fail)", () => {
        objects.forEach((d) => {
            const s = `there is a ${d}`;
            semanticMatchFailTest(s, "Conditional");
            semanticMatchFailTest(s, "ThereIsAnObjectConditional");
        });
    });
    it("Named without context (a)", () => {
        objects.forEach((d) => {
            const s = `there is a ${d} "newPart123"`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsAnObjectConditional");
        });
    });
    it("Named without context (an)", () => {
        objects.forEach((d) => {
            const s = `there is an ${d} "newPart123"`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsAnObjectConditional");
        });
    });
});
describe("'there is not a' ObjectSpecifier" , () => {
    it ("Basic (current card)", () => {
        const s = `there is not a current card`;
        semanticMatchTest(s, "Conditional");
        semanticMatchTest(s, "ThereIsNotAnObjectConditional");
    });
    it ("Basic (wth id)", () => {
        objects.forEach((d) => {
            const s = `there is not a ${d} 20`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsNotAnObjectConditional");
        });
    });
    it("no target or context (should fail)", () => {
        objects.forEach((d) => {
            const s = `there is not a ${d}`;
            semanticMatchFailTest(s, "Conditional");
            semanticMatchFailTest(s, "ThereIsNotAnObjectConditional");
        });
    });
    it("Named without context (a)", () => {
        objects.forEach((d) => {
            const s = `there is not a ${d} "newPart123"`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsNotAnObjectConditional");
        });
    });
    it("Named without context (an)", () => {
        objects.forEach((d) => {
            const s = `there is not an ${d} "newPart123"`;
            semanticMatchTest(s, "Conditional");
            semanticMatchTest(s, "ThereIsNotAnObjectConditional");
        });
    });
});

describe("there is a property" , () => {
    it ("without specifier", () => {
        const s = `there is a property "super prop"`;
        semanticMatchTest(s, "Conditional");
        semanticMatchTest(s, "ThereIsAPropertyConditional");
        semanticMatchTest(s, "ThereIsAPropertyConditional_withoutSpecifier");
    });
    it ("with specifier", () => {
        const s = `there is a property "super prop" of first button of current card`;
        semanticMatchTest(s, "Conditional");
        semanticMatchTest(s, "ThereIsAPropertyConditional");
        semanticMatchTest(s, "ThereIsAPropertyConditional_withSpecifier");
    });
});
describe("there is not a property" , () => {
    it ("without specifier", () => {
        const s = `there is not a property "super prop"`;
        semanticMatchTest(s, "Conditional");
        semanticMatchTest(s, "ThereIsNotAPropertyConditional");
        semanticMatchTest(s, "ThereIsNotAPropertyConditional_withoutSpecifier");
    });
    it ("with specifier", () => {
        const s = `there is not a property "super prop" of first button of current card`;
        semanticMatchTest(s, "Conditional");
        semanticMatchTest(s, "ThereIsNotAPropertyConditional");
        semanticMatchTest(s, "ThereIsNotAPropertyConditional_withSpecifier");
    });
});


<|start_filename|>js/ohm/tests/test-conditional-grammar.js<|end_filename|>
/**
 * Conditional Grammar Tests
 * Conditional is a rule and series of sub-
 * rules describing a specific arrangement of
 * expressions which, when evaluated, result in
 * a boolean value.
 * We additionally include here Comparison rules,
 * which are used in the construction of Conditional.
 */
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
};
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};


describe("Basic Literal Conditional Tests", () => {
    describe("Equality via = sign", () => {
        it("Can deal with Integer literals", () => {
            let str = `2 == 4`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
        it("Can deal with Float literals", () => {
            let str = `0.1409 == 22.4`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
        it("Can deal with String literals", () => {
            let str = `"this is the first string" == "this is the second string"`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
    });
    describe("Equality via 'is'", () => {
        it("Can deal with Integer literals", () => {
            let str = `2 is 4`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
        it("Can deal with Float literals", () => {
            let str = `0.44 is -2.3`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
        it("Can deal with String literals", () => {
            let str = `"string one" is "string two"`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
    });
    describe("Non-equality via '!='", () => {
        it("Can deal with Integer literals", () => {
            let str = `2 != 3`;
            semanticMatchTest(str, "NonEqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
        it("Can deal with Float literals", () => {
            let str = `-0.1 != -0.1`;
            semanticMatchTest(str, "NonEqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
        it("Can deal with String literals", () => {
            let str = `"this is first" != "this is second"`;
            semanticMatchTest(str, "NonEqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
    });
    describe("Non-equality via 'is not'", () => {
        // Note that this is an EqualityConditional
        // because '"not" Expression' is a valid Factor already,
        // so it fits the 'Expression "is" Expression' rule
        it("Can deal with Integer literals", () => {
            let str = `2 is not 3`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
        it("Can deal with Float literals", () => {
            let str = `0.1 is not -223.5`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
        it("Can deal with String literals", () => {
            let str = `"this is first" is not "this is second"`;
            semanticMatchTest(str, "EqualityConditional");
            semanticMatchTest(str, "Conditional");
        });
    });
    describe("Kind Equality via 'is a'", () => {
        it.skip("Add Kind Equality Tests and more");
    });
});


<|start_filename|>js/ohm/tests/test-repeat-grammar.js<|end_filename|>
/**
 * Repeat Grammar Tests
 * --------------------------------
 * Tests the grammar of looping constructs
 */
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
};
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};

const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
};

describe("RepeatControlForm tests", () => {
    describe("forNumTimes", () => {
        it("Can do basic repeat of literal integer", () => {
            let str = "repeat for 5 times";
            semanticMatchTest(str, "RepeatControlForm_forNumTimes");
        });
        it("Can do basic repeat of variable (assume integer)", () => {
            let str = "repeat for myCustomNum times";
            semanticMatchTest(str, "RepeatControlForm_forNumTimes");
        });
        it("Fails when attempting to do a float literal", () => {
            let str = "repeat for 0.5 times";
            semanticMatchFailTest(str, "RepeatControlForm_forNumTimes");
        });
        it("Fails when attemting to use a string literal", () => {
            let str = `repeat for "3" times`;
            semanticMatchFailTest(str, "RepeatControlForm_forNumTimes");
        });
    });
    describe("untilCondition", () => {
        it("Can do a boolean literal", () => {
            let str = `repeat until true`;
            semanticMatchTest(str, "RepeatControlForm_untilCondition");
        });
        it("Can do a variable name as the condition", () => {
            let str = `repeat until myVariable`;
            semanticMatchTest(str, "RepeatControlForm_untilCondition");
        });
        it("Can use a conditional statement of literals", () => {
            let str = `repeat until 2 > 3`;
            semanticMatchTest(str, "RepeatControlForm_untilCondition");
        });
        it("Can use a conditional that has variable name(s)", () => {
            let str = `repeat until myVariable is 5`;
            semanticMatchTest(str, "RepeatControlForm_untilCondition");
        });
    });
    describe("whileCondition", () => {
       it("Can do a boolean literal", () => {
            let str = `repeat while true`;
            semanticMatchTest(str, "RepeatControlForm_whileCondition");
        });
        it("Can do a variable name as the condition", () => {
            let str = `repeat while myVariable`;
            semanticMatchTest(str, "RepeatControlForm_whileCondition");
        });
        it("Can use a conditional statement of literals", () => {
            let str = `repeat while 2 > 3`;
            semanticMatchTest(str, "RepeatControlForm_whileCondition");
        });
        it("Can use a conditional that has variable name(s)", () => {
            let str = `repeat while myVariable is 5`;
            semanticMatchTest(str, "RepeatControlForm_whileCondition");
        }); 
    });
    describe("withStartFinish", () => {
        it("Can use a basic variable name with literal integer range", () => {
            let str = `repeat with myNum = 0 to 100`;
            semanticMatchTest(str, "RepeatControlForm_withStartFinish");
        });
    });
    describe("forever", () => {
        it("Can understand the forever repeat", () => {
            let str = `repeat `;
            semanticMatchTest(str, "RepeatControlForm_forever");
        });
    });
});

describe("RepeatBlock tests", () => {
    it("forNumTimes", () => {
        let str = [
            "repeat for 5 times",
            "doSomething",
            "go to next card",
            "end repeat"
        ].join("\n");
        semanticMatchTest(str, "RepeatBlock");
    });
    it('untilCondition', () => {
        let str = [
            "repeat until myVariable <= 20.5",
            "doSomething myVariable",
            "end repeat"
        ].join("\n");
        semanticMatchTest(str, "RepeatBlock");
    });
    it("whileCondition", () => {
        let str = [
            "repeat while myVariable >= 20.5",
            "doSomething myVariable",
            "end repeat"
        ].join("\n");
        semanticMatchTest(str, "RepeatBlock");
    });
    it("withStartFinish", () => {
        let str = [
            "repeat with myNum = 0 to 6",
            "doSomething myVariable",
            "end repeat"
        ].join("\n");
        semanticMatchTest(str, "RepeatBlock");
    });
    it("forever", () => {
        let str = [
            "repeat",
            "doSomething myVariable",
            "end repeat"
        ].join("\n");
        semanticMatchTest(str, "RepeatBlock");
    });
});

describe("Repeat full script tests", () => {
    it("forNumTimes", () => {
        let script = [
            "on click",
            "put 0 into myNum",
            "repeat for 5 times",
            "put (myNum + 1) into myNum",
            "end repeat",
            "end click"
        ].join("\n");
        semanticMatchTest(script, "MessageHandler");
        semanticMatchTest(script, "Script");
    });
    it("untilCondition", () => {
        let script = [
            "on click",
            "\tput 5 into myLimit",
            "\tput 0 into myCount",
            "\trepeat until myLimit <= 0",
            "\tput (myCount + 1) into myCount",
            "\tput (myLimit - 1) into myLimit",
            "\tend repeat",
            "end click"
        ].join("\n");
        semanticMatchTest(script, "Script");
        semanticMatchTest(script, "MessageHandler");
    });
});

describe("Misc full script repeat tests", () => {
    it("Can parse an exit repeat in nested if block", () => {
        let script = [
        "on click",
        "put 0 into Counter",
        "repeat until Counter == 5",
        "answer Counter",
        "put (Counter + 1) into Counter",
        "if Counter >= 3 then exit repeat",
        "end repeat",
        "end click"
        ].join("\n");
        semanticMatchTest(script, "Script");
        semanticMatchTest(script, "MessageHandler");
    });
});


<|start_filename|>js/objects/parts/Part.js<|end_filename|>
/**
 * Part
 * -------------------------------
 * I represent the prototype object for all
 * SimpleTalk parts.
 */
import {
    idMaker,
    isValidId
} from '../utils/id.js';
import errorHandler from '../utils/errorHandler.js';
import {
    PartProperties,
    BasicProperty,
    DynamicProperty
} from '../properties/PartProperties.js';

import {ActivationContext} from '../ExecutionStack.js';


class Part {
    constructor(anOwnerPart, name, deserializing=false){

        this.name = this.constructor.name;

        // An array of child parts
        this.subparts = [];
        // a list of all accepted subparts by type
        // By default this is null and each Part subclcass should
        // specify if otherwise
        this.acceptedSubpartTypes = [];

        this.partProperties = new PartProperties();
        this._owner = anOwnerPart;
        this._commandHandlers = {};
        this._privateCommandHandlers = {};
        this._functionHandlers = {};
        this._scriptSemantics = {};
        this._propertySubscribers = new Set();
        this._viewSubscribers = new Set();
        this._stepIntervalId = null;

        this.isPart = true;

        // Bind methods
        this.setupProperties = this.setupProperties.bind(this);
        this.setupStyleProperties = this.setupStyleProperties.bind(this);

        this.addPart = this.addPart.bind(this);
        this.removePart = this.removePart.bind(this);
        this.acceptsSubpart = this.acceptsSubpart.bind(this);
        this.setPrivateCommandHandler = this.setPrivateCommandHandler.bind(this);
        this.removePrivateCommandHandler = this.removePrivateCommandHandler.bind(this);
        this.setFuncHandler = this.setFuncHandler.bind(this);
        this.receiveCmd = this.receiveCmd.bind(this);
        this.receiveFunc = this.receiveFunc.bind(this);
        this.receiveError = this.receiveError.bind(this);
        this.receiveMessage = this.receiveMessage.bind(this);
        this.delegateMessage = this.delegateMessage.bind(this);
        this.sendMessage = this.sendMessage.bind(this);
        this.addPropertySubscriber = this.addPropertySubscriber.bind(this);
        this.removePropertySubscriber = this.removePropertySubscriber.bind(this);
        this.addViewSubscriber = this.addViewSubscriber.bind(this);
        this.removeViewSubscriber = this.removeViewSubscriber.bind(this);
        this.serialize = this.serialize.bind(this);
        this.toJSON = this.toJSON.bind(this);
        this.setPropsFromDeserializer = this.setPropsFromDeserializer.bind(this);
        this.findAncestorOfType = this.findAncestorOfType.bind(this);
        this.openEditorCmdHandler = this.openEditorCmdHandler.bind(this);
        this.closeEditorCmdHandler = this.closeEditorCmdHandler.bind(this);
        this.copyCmdHandler = this.copyCmdHandler.bind(this);
        this.pasteCmdHandler = this.pasteCmdHandler.bind(this);
        this.isSubpartOfCurrentCard = this.isSubpartOfCurrentCard.bind(this);
        this.isSubpartOfCurrentStack = this.isSubpartOfCurrentStack.bind(this);
        this.getOwnerBranch = this.getOwnerBranch.bind(this);
        this.startStepping = this.startStepping.bind(this);
        this.stopStepping = this.stopStepping.bind(this);
        this.setTargetProp = this.setTargetProp.bind(this);
        this.move = this.move.bind(this);
        this.moveSubpartUp = this.moveSubpartUp.bind(this);
        this.moveSubpartDown = this.moveSubpartDown.bind(this);
        this.moveSubpartToFirst = this.moveSubpartToFirst.bind(this);
        this.moveSubpartToLast = this.moveSubpartToLast.bind(this);



        // Finally, we finish initialization
        this.setupProperties();

        // command handlers
        this.setPrivateCommandHandler("openEditor", this.openEditorCmdHandler);
        this.setPrivateCommandHandler("closeEditor", this.closeEditorCmdHandler);
        this.setPrivateCommandHandler("openHalo", () => {this.partProperties.setPropertyNamed(this, "halo-open", true);});
        this.setPrivateCommandHandler("closeHalo", () => {this.partProperties.setPropertyNamed(this, "halo-open", false);});
        this.setPrivateCommandHandler("setTargetTo", this.setTargetProp);
        this.setPrivateCommandHandler("copy", this.copyCmdHandler);
        this.setPrivateCommandHandler("paste", this.pasteCmdHandler);
        this.setPrivateCommandHandler("move", this.move);
        this.setPrivateCommandHandler("moveUp", () => {this._owner.moveSubpartUp(this);});
        this.setPrivateCommandHandler("moveDown", () => {this._owner.moveSubpartDown(this);});
        this.setPrivateCommandHandler("moveToFirst", () => {this._owner.moveSubpartToFirst(this);});
        this.setPrivateCommandHandler("moveToLast", () => {this._owner.moveSubpartToLast(this);});
    }

    // Convenience getter to get the id
    // from the partProperties
    get id(){
        return this.partProperties.getPropertyNamed(this, 'id');
    }

    set id(val){
        return this.partProperties.setPropertyNamed(this, 'id', val);
    }


    // Return an array of names of all of my and my ancestors' handlers
    // for the moment this is just names, type, id and whether the handler overrides
    // an owner's, but could be richer info, such as arguments, documentation etc
    get commandHandlerRegistry(){
        let handlersInfo = {};
        let ownerBranch = this.getOwnerBranch();
        for(let i = 1; i <= ownerBranch.length; i++){
            let part = ownerBranch[ownerBranch.length - i];
            let partType = part.type;
            if(part.id === -1){
                partType = "System";
            } else {
                // System doesn't have private command handlers
                Object.keys(part._privateCommandHandlers).forEach((h) => {
                    let override = false;
                    if(handlersInfo[h]){
                        override = true;
                    }
                    handlersInfo[h] = {partId: part.id, partType: partType, override: override, private: true};
                });
            }
            Object.keys(part._commandHandlers).forEach((h) => {
                let override = false;
                if(handlersInfo[h]){
                    override = true;
                }
                handlersInfo[h] = {partId: part.id, partType: partType, override: override, private: false};
            });
        }
        return handlersInfo;
    }

    // returns the this.part -> System branch by part id
    getOwnerBranch(branch){
        if(!branch){
            branch = [this];
        }
        if(this.type === "world"){
            branch.push(window.System);
            return branch;
        } else {
            branch.push(this._owner);
        };
        return this._owner.getOwnerBranch(branch);
    }

    // Configures the specific properties that the
    // given part can expect, along with any default
    // values.
    // Descendant Parts should override this method
    // in their own constructor after calling super,
    // so that they get the parent's general properties
    // too.
    setupProperties(){
        // Here, we set up properties common
        // to ALL Parts in the system.
        let basicProps = [
            new BasicProperty(
                'target',
                null,
            ),
            new BasicProperty(
                'contents',
                null,
            ),
            new BasicProperty(
                'enabled',
                true
            ),
            new BasicProperty(
                'wants-move',
                false
            ),
            new BasicProperty(
                'id',
                idMaker.new()
            ),
            new BasicProperty(
                'name',
                `New ${this.type}`
            ),
            new BasicProperty(
                'rectangle',
                "0, 0, 0, 0",
                true,
                ['rect']
            ),
            new BasicProperty(
                'script',
                null // For now
            ),

            // Styling
            // css (really JS style) key-values
            new BasicProperty(
                'cssStyle',
                {},
                false,
            ),
            // css (really JS style) key-values
            new BasicProperty(
                'cssTextStyle',
                {},
                false,
            )
        ];
        basicProps.forEach(prop => {
            this.partProperties.addProperty(prop);
        });

        this.partProperties.newDynamicProp(
            'halo-open',
            function(propOwner, propObject, val){
                // close all other halos
                // (there really should only be one open at a time)
                if(val){
                    Array.from(document.querySelectorAll('.editing')).forEach(el => {
                        el.model.partProperties.setPropertyNamed(el.model, "halo-open", false);
                    });
                }
                propObject._value = val;
            },
            function(propOwner, propObject){
                return propObject._value;
            },
            false, // read & write
            false  // default value
        );
        this.partProperties.newDynamicProp(
            'editor-open',
            function(propOwner, propObject, val){
                if(val){
                    // open the halo on this part if it accepts halos
                    let haloOpenProp = propOwner.partProperties.findPropertyNamed("halo-open");
                    if(haloOpenProp){
                        haloOpenProp.setValue(propOwner, true);
                    } else {
                        // even if the part does not accept halos (like card, stack etc)
                        // we should still close them elsewhere
                        Array.from(document.querySelectorAll('.editing')).forEach(el => {
                            el.model.partProperties.setPropertyNamed(el.model, "halo-open", false);
                        });
                    }
                }
                propObject._value = val;
            },
            function(propOwner, propObject){
                return propObject._value;
            },
            false, // read & write
            false  // default value
        );
        // the index number of the part in part._owner.subpart
        // array. Note: this is 1-indexed
        this.partProperties.newDynamicProp(
            'number',
            null, // no setter
            function(propOwner, propObject){
                if(propOwner.type == "world"){
                    return 1;
                }
                return propOwner._owner.subparts.indexOf(propOwner) + 1;
            },
            true // readonly
        );

        this.partProperties.newDynamicProp(
            'target',
            function(propOwner, propObject, val){
                // check to see if the target is a non-ID
                let id = isValidId(val);
                if(id){
                    // if it is an ID insert "part" since our
                    // grammar doesn't handle ID without system object
                    // prefixes
                    propObject._value = `part id ${val}`;
                } else {
                    propObject._value = val;
                }
            },
            function(propOwner, propObject){
                return propObject._value;
            },
            false,
            null,
        ),

        // Custom Properties store props defined within the
        // ST environment
        this.partProperties.newCustomProp(
        ),

        // Stepping related props

        this.partProperties.newDynamicProp(
            // The time in milliseconds between
            // sends of the step command if the
            // stepping property is set to true
            'stepTime',
            // Dynamic setter
            function(propOwner, propObject, value){
                if(propOwner.isStepping){
                    // Interrupt the current interval
                    // and restart with new stepTime
                    propOwner.stopStepping();
                    this._value = value;
                    propOwner.startStepping();
                } else{
                    this._value = value;
                }
            },
            // Dynamic getter
            function(propOwner, propObject){
                return this._value;
            },
            false, // can read and write
            500 // Default to half a second
        );

        this.partProperties.newDynamicProp(
            'stepping',
            // Dynamic setter
            function(propOwner, propObject, value){
                if(value === false && propOwner.isStepping){
                    propOwner.stopStepping();
                } else if(value === true && !propOwner.isStepping){
                    propOwner.startStepping();
                }
            },
            // Dynamic getter
            function(propOwner, propObject){
                // If the intervalId is set, then
                // the Part is currently stepping
                return propOwner.isStepping;
            },
        );

    }

    // To be called in each sub-class that has StyleProperties
    // called after the style props are configured
    setupStyleProperties(){
        this.partProperties._properties.forEach((prop) => {
            if(prop.constructor.name === "StyleProperty"){
                // setting the value on itself ensures that the cssStyle
                // BasicProperty is updated with the appropriate styler
                // conversion css key-val
                prop.setValue(this, prop._value);
            }
        });
    }

    /** Subpart Access **/
    /**
     * Each subclass will implement its own set of checks,
     * and throw an approprite error if the subpart type is invalid.
     */
    acceptsSubpart(aPartType){
        if (this.acceptedSubpartTypes[0] === "*"){
            return true;
        }
        return this.acceptedSubpartTypes.includes(aPartType.toLowerCase());
    }

    /**
     * Adds a part to this part's subparts
     * collection, if not already present.
     * It will also set the owner of the
     * added part to be this part.
     */
    addPart(aPart){
        if(!this.acceptsSubpart(aPart.type)){
            throw new Error(`${this.type} does not accept subparts of type ${aPart.type}`);
        }

        let found = this.subparts.indexOf(aPart);
        if(found < 0){
            this.subparts.push(aPart);
            aPart._owner = this;
        }
    }

    /**
     * Removes the given part from this
     * part's list of subparts (if present).
     * It will also unset the owner of the
     * given part.
     */
    removePart(aPart){
        let partIndex = this.subparts.indexOf(aPart);
        if(partIndex >= 0){
            this.subparts.splice(partIndex, 1);
            aPart._owner = null;
        }
    }

    /** Checks whether the Part instance is a subpart of the current
     * Card.
     */
    isSubpartOfCurrentCard(){
    }

    /** Checks whether the Part instance is a subpart of the current
     * Stack.
     */
    isSubpartOfCurrentStack(){
    }

    /** Logging and Reporting **/
    shouldBeImplemented(functionName){
        let msg = `${this.constructor.name} should implement ${functionName}`;
        throw new Error(msg);
    }

    /** Message Handling and Delegation **/
    delegateMessage(aMessage){
        return this.sendMessage(
            aMessage,
            this._owner
        );
    }

    sendMessage(aMessage, target){
        return window.System.sendMessage(aMessage, this, target);
    }

    receiveMessage(aMessage){
        // By default, Parts will only handle
        // messages of type 'command' and 'function'
        switch(aMessage.type){
            case 'command':
                return this.receiveCmd(aMessage);
                //break;
            case 'function':
                return this.receiveFunc(aMessage);
                //break;
            case 'error':
                return this.receiveError(aMessage);
            default:
                return this.delegateMessage(aMessage);
        }
    }

    receiveError(aMessage){
        return errorHandler.handle(aMessage);
    }

    receiveCmd(aMessage){
        let handler = this._commandHandlers[aMessage.commandName];
        if(handler){
            // If this Part has a handler for
            // the given command, we run it.
            // We also late-bind the current part
            // instance as the 'this' context for
            // the handler
            let boundHandler = handler.bind(this);
            var activation = new ActivationContext(
                aMessage.commandName,
                this,
                aMessage,
                boundHandler
            );
            window.System.executionStack.push(activation);
            var result = boundHandler(aMessage.senders, ...aMessage.args);
            window.System.executionStack.pop();
            return result;
        }

        let privateHandler = this._privateCommandHandlers[aMessage.commandName];
        if(privateHandler){
            // If this Part has a handler for
            // the given command, we run it.
            // We also late-bind the current part
            // instance as the 'this' context for
            // the handler
            let boundHandler = privateHandler.bind(this);
            var activation = new ActivationContext(
                aMessage.commandName,
                this,
                aMessage,
                boundHandler
            );
            window.System.executionStack.push(activation);
            var result = boundHandler(aMessage.senders, ...aMessage.args);
            window.System.executionStack.pop();
            return result;
        }

        // Otherwise, we have no handler for
        // it. Unless the message indicates shouldNotDelegate
        // we delegate along the
        // message delegation chain. It is up
        // to Parts to properly implement delegation
        // for themselves!
        if(aMessage.shouldNotDelegate){
            return aMessage;
        }
        return this.delegateMessage(aMessage);
    }

    receiveFunc(aMessage){
        let handler = this._functionHandlers[aMessage.functionName];

        if(handler){
            let boundHandler = handler.bind(this);
            return boundHandler();
        } else {
            return this.delegateMessage(aMessage);
        }
    }

    setPrivateCommandHandler(commandName, handler){
        this._privateCommandHandlers[commandName] = handler;
    }

    removePrivateCommandHandler(commandName){
        delete this._privateCommandHandlers[commandName];
    }

    setFuncHandler(funcName, handler){
        this._functionHandlers[funcName] = handler;
    }

    /** Command Handlers
        ----------------
        Command handlers which are invoked at the Part level
        which are not immediately delegaed to the Part._owner
    **/

    openEditorCmdHandler(){
        this.partProperties.setPropertyNamed(this, "editor-open", true);
    }

    closeEditorCmdHandler(){
        this.partProperties.setPropertyNamed(this, "editor-open", false);
    }

    setTargetProp(senders, ...args){
        let target = args.join(" ");
        this.partProperties.setPropertyNamed(this, "target", target);
    }

    copyCmdHandler(){
        window.System.clipboard.copyPart(this);
    }

    pasteCmdHandler(){
        if(!window.System.clipboard.isEmpty){
            let item = window.System.clipboard.contents[0];
            if(item.type == 'simpletalk/json' && this.acceptsSubpart(item.partType)){
                window.System.clipboard.pasteContentsInto(this);
            }
        }
    }

    move(senders, movementX, movementY){
        if(!this.partProperties.getPropertyNamed(this, "wants-move")){
            throw Error(`Part ${this.id} trying to move with 'wants-move' property false`);
        }
        let top = this.partProperties.getPropertyNamed(this, "top");
        top += movementY;
        this.partProperties.setPropertyNamed(this, "top", top);
        let left = this.partProperties.getPropertyNamed(this, "left");
        left += movementX;
        this.partProperties.setPropertyNamed(this, "left", left);
    }

    moveSubpartDown(part){
        let currentIndex = this.subparts.indexOf(part);
        if(currentIndex < this.subparts.length - 1){
            this.subpartOrderChanged(part.id, currentIndex, currentIndex + 1);
        }
    }

    moveSubpartUp(part){
        let currentIndex = this.subparts.indexOf(part);
        if(currentIndex > 0){
            this.subpartOrderChanged(part.id, currentIndex, currentIndex - 1);
        }
    }

    // Note: moveSubpartToFirst means move to first in the view
    // i.e. last as a subaprt
    moveSubpartToFirst(part){
        let currentIndex = this.subparts.indexOf(part);
        this.subpartOrderChanged(part.id, currentIndex, 0);
    }

    moveSubpartToLast(part){
        let currentIndex = this.subparts.indexOf(part);
        this.subpartOrderChanged(part.id, currentIndex, this.subparts.length - 1);
    }

    /** Property Subscribers
        ------------------------
        Objects added as property subscribers
        will be 'notified' whenever one of this
        Part's properties changes
    **/
    addPropertySubscriber(anObject){
        this._propertySubscribers.add(anObject);
    }

    removePropertySubscriber(anObject){
        this._propertySubscribers.delete(anObject);
    }

    propertyChanged(propertyName, newValue){
        let message = {
            type: 'propertyChanged',
            propertyName: propertyName,
            value: newValue,
            partId: this.id
        };
        this._propertySubscribers.forEach(subscriber => {
            this.sendMessage(message, subscriber);
        });
    }

    /** View Subscribers
        ------------------------
        Objects added as view subscribers
        will be 'notified' whenever this Part
        incurrs a view change (add, delete subparts, reorder etc)
    **/
    addViewSubscriber(anObject){
        this._viewSubscribers.add(anObject);
    }

    removeViewSubscriber(anObject){
        this._viewSubscribers.delete(anObject);
    }

    viewChanged(changeName, ...args){
        let message = {
            type: 'viewChanged',
            changeName: changeName,
            partId: this.id,
            args: args
        };
        this._viewSubscribers.forEach(subscriber => {
            this.sendMessage(message, subscriber);
        });
    }

    subpartOrderChanged(id, currentIndex, newIndex){
        let subpart = this.subparts.splice(currentIndex, 1)[0];
        this.subparts.splice(newIndex, 0, subpart);
        this.viewChanged("subpart-order", id, currentIndex, newIndex);
    }

    startStepping(){
        if(this._stepIntervalId){
            this.stopStepping();
        }
        let stepTime = this.partProperties.getPropertyNamed(
            this,
            'stepTime'
        );
        if(stepTime > 0){
            this._stepIntervalId = setInterval(() => {
                this.sendMessage({
                    type: 'command',
                    commandName: 'step',
                    args: []
                }, this);
            }, stepTime);
        }
    }

    stopStepping(){
        clearInterval(this._stepIntervalId);
        this._stepIntervalId = null;
    }

    get isStepping(){
        // We know the Part is currently stepping
        // of the stored intervalId is set to
        // something besides null
        return this._stepIntervalId !== null;
    }

    /**
     * Serialize this Part's state as JSON.
     * By default, we do not serialize specific
     * PartCollection information (recursively),
     * and only include basics including the current
     * state of all properties.
     */
    serialize(){
        let ownerId = null;
        if(this._owner){
            ownerId = this._owner.id;
        }
        let result = {
            type: this.type,
            id: this.id,
            properties: {},
            subparts: this.subparts.map(subpart => {
                return subpart.id;
            }),
            ownerId: ownerId
        };
        this.partProperties._properties.filter(prop => {
            // we make sure to only serialize those properties which have non-default values
            // with the exception of the "custom-properties" prop which we always serialize
            return (prop.name == "custom-properties" || prop.getValue(this) !== prop.default);
        }).forEach(prop => {
            let name = prop.name;
            // due to race-conditions incurred if the editor is
            // open before world we exclude `editor-open` from serialization
            if(name == "editor-open"){
                return;
            }
            let value = prop.getValue(this);
            result.properties[name] = value;
        });
        return result;
    }

    /**
     * Set the properties and other
     * attributes of this Part model
     * from a deserialized JSON object.
     */
    setPropsFromDeserializer(incomingProps, deserializer){
        Object.keys(incomingProps).forEach(propName => {
            let property = this.partProperties.findPropertyNamed(propName);
            if(!property){
                // If some old or invalid property is
                // present in the deserialization, simply provide
                // a warning and then skip this one.
                console.warn(`Deserialized property "${propName}" is not a valid property name for ${this.type} (id ${this.id}) and will be ignored`);
            } else if(propName == "custom-properties"){
                // custom properties are serialized as an object like other props
                // and we need to create properties from these and set their respective
                // values. Then we need to set the value of "custom-properties" prop
                // itself to be the object containing all of these
                let customPropsData = incomingProps[propName];
                let newCustomPropsObject = {};
                Object.values(customPropsData).forEach((propData) => {
                    let newProp = new BasicProperty(propData.name, null);
                    newProp.setValue(this, propData._value, false); // no need to notify
                    newCustomPropsObject[propData.name] = newProp;
                });
                property.setValue(this, newCustomPropsObject, false); // no need to notify
            } else if(!property.readOnly){
                // Last arg is false, which tells the property
                // not to notify its owner's subscribers of
                // property changes. We don't need that when
                // deserializing
                property.setValue(this, incomingProps[propName], false);
            }
        });
    }

    toJSON(){
        return this.serialize();
    }

    findAncestorOfType(aPartType){
        let owner = this._owner;
        while(owner){
            if(owner.type == aPartType){
                return owner;
            }
            owner = owner._owner;
        }
        return null;
    }
};

export {
    Part,
    Part as default
};


<|start_filename|>js/objects/tests/test-string-concatenation-with-preload.js<|end_filename|>
import chai from 'chai';
import ohm from 'ohm-js';
const assert = chai.assert;
const expect = chai.expect;

import interpreterSemantics from '../../ohm/interpreter-semantics.js';

let testLanguageGrammar = ohm.grammar(window.grammar);

describe("String Concatenation Interpreter Tests", () => {
    let semantics;
    before(() => {
        semantics = testLanguageGrammar.createSemantics();
        semantics.addOperation(
            'interpret',
            interpreterSemantics(null, window.System)
        );
    });

    it("Can concatenate two string literals", () => {
        let input = `"Hello, " & "world!"`;
        let expected = "Hello, world!";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });

    it("Can concatenate an integer literal and a string literal", () => {
        let input = `-2 & " times!"`;
        let expected = "-2 times!";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });

    it("Can concatenate two integer literals", () => {
        let input = `-2 & 3`;
        let expected = "-23";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });

    it("Can concatenate an expression and a string literal", () => {
        let input = `"Value is: " & (-1 * (5 + 2.01))`;
        let expected = "Value is: -7.01";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });

    it("Can concatenate a complex example (4 items)", () => {
        let input = `"Hello, you have " & -2 & " items available and " & (2 + (3 * 4)) & " points!"`;
        let expected = "Hello, you have -2 items available and 14 points!";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });
});


<|start_filename|>js/objects/tests/test-error-handling-with-preload.js<|end_filename|>
/**
 * Tests for Error Handling 
 * -------------------------------------------
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

let currentCard;
let button;
let anotherButton;
describe('Error Handling', () => {
    describe('Setup', () => {
        it('Can add a new button to the current card', () => {
            let makeButtonFunction = function(){
                currentCard = System.getCurrentCardModel();
                let msg = {
                    type: 'command',
                    commandName: 'newModel',
                    args: ['button', currentCard.id]
                };
                currentCard.sendMessage(msg, currentCard);
            };
            expect(makeButtonFunction).to.not.throw();
        });
        it('Can find the button object, which has NO command handlers yet', () => {
            button = System.getCurrentCardModel().subparts.find(subpart => {
                return subpart.type == 'button';
            });
            assert.exists(button);
            assert.isEmpty(button._commandHandlers);
        });
    });
    describe('GrammarMatchError', () => {
        it('GrammarMatchError automatically opens the script editor (if not present)', () => {
            let firstScript = [
                'on doSomethingFirst',
                'not a command',
                'end doSomethingFirst',
            ].join('\n');
            button.partProperties.setPropertyNamed(button, "script", firstScript);
            let scriptEditor = window.System.findScriptEditorByTargetId(button.id);
            assert.isNotNull(scriptEditor);
        });
        it.skip('Script editor has the propertly error marked content', () => {
           let markedUpScript = [
                'on doSomethingFirst',
                'not a command --<<<[Expected:"end"; ruleName: "StatementList"]',
                'end doSomethingFirst',
            ].join('\n');
            let scriptEditor = window.System.findScriptEditorByTargetId(button.id);
            let text = scriptEditor.innerText;
            assert.equal(text, markedUpScript);
        });
    });
    describe('MessageNotUnderstood', () => {
        before(() => {
            // add a grammatically correct script that reference an unkown command
            let firstScript = [
                'on click',
                'someNotACommandCommand',
                'end click',
            ].join('\n');
            // we add a whole new button b/c at the moment our scripting field is not
            // responsive
            anotherButton = window.System.newModel("button", currentCard.id);
            anotherButton.partProperties.setPropertyNamed(anotherButton, "script", firstScript);
        });

        it('Sending a MessageNotUnderstood', () => {
            let sendErrorMsgFunction = function(){
                let MNUmsg = {
                    type: "error",
                    name: "MessageNotUnderstood",
                    message: {
                        type: "command",
                        commandName: "someNotACommandCommand",
                        args: [],
                        senders: [{name: "Button", id: anotherButton.id}]
                    }
                };
                anotherButton.sendMessage(MNUmsg, anotherButton);
            };
            expect(sendErrorMsgFunction).to.not.throw();
        });

        it.skip('MessageNotUnderstood command is marked up in the editor', () => {
            let markedUpScript = [
                'on click',
                'someNotACommandCommand --<<<[MessageNotUnderstood: command; commandName: "someNotACommandCommand"]',
                'end click',
            ].join('\n');
            let scriptEditor = window.System.findScriptEditorByTargetId(anotherButton.id);
            let text = scriptEditor.innerText;
            assert.equal(text, markedUpScript);
        });
    });
});


<|start_filename|>js/objects/tests/test-stack-with-preload.js<|end_filename|>
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

let exampleButtonScript = `
on click
    put 1 into myVar
    doFirstNested
    set "name" to myVar
end click

on doFirstNested
    put 2 into myVar
    doSecondNested
end doFirstNested

on doSecondNested
    put 3 into myVar
    doThirdNested
end doSecondNested

on doThirdNested
    put 4 into myVar
end doThirdNested
`;

let buttonModel;
let currentCardModel;

function compileButtonScript(aScript){
    let msg = {
        type: 'compile',
        codeString: aScript,
        targetId: buttonModel.id
    };
    window.System.compile(msg);
};

function sendButtonClick(){
    let clickMessage = {
        type: 'command',
        commandName: 'click',
        args: []
    };
    buttonModel.sendMessage(clickMessage, buttonModel);
};

describe("Test nested stack execution of messages on single button part", () => {
    describe("Model Setup", () => {
        it('Can add a button to current card model without error', () => {
            currentCardModel = window.System.getCurrentCardModel();
            let addButton = function(){
                let msg = {
                    type: 'command',
                    commandName: 'newModel',
                    args: [
                        'button',
                        currentCardModel.id,
                        'card'
                    ]
                };
                currentCardModel.sendMessage(msg, currentCardModel);
            };
            expect(addButton).to.not.throw(Error);
        });
        it('Can find newly created button model', () => {
            let button = currentCardModel.subparts.filter(part => {
                return part.type == 'button';
            })[0];
            buttonModel = button;
            assert.exists(button);
        });
    });
    describe("Can properly map vars in nested messages on button", () => {
        it('Can compile example script without error', () => {
            let compileFunction = () => {
                compileButtonScript(exampleButtonScript);
            };
            expect(compileFunction).to.not.throw(Error);
        });
        it('Sets button name to correct value when clicked', () => {
            // We expect the result of myVar in the outer
            // click handler to still be 1, and so the
            // name of the button should be set to the
            // string "1"
            sendButtonClick();
            let name = buttonModel.partProperties.getPropertyNamed(
                buttonModel,
                "name"
            );
            assert.equal(name, "1");
        });
    });
});


<|start_filename|>webpack.config.js<|end_filename|>
const path = require('path');

module.exports = {
    devtool: 'inline-source-map',
    mode: 'development',
    entry: {
        devSystem: './js/objects/System.js',
    },
    output: {
        path: path.resolve(__dirname, 'js', 'objects', 'build'),
        filename: '[name].bundle.js'
    }
};


<|start_filename|>js/objects/views/contextmenu/ContextMenuItem.js<|end_filename|>
const templateString = `
<style>
    :host {
        display: flex;
        position: relative;
    }
    .submenu-area {
        display: none;
        position: absolute;
        left: 100%;
        top: 0px;
    }

    :host(:hover) .submenu-area {
        display: flex;
    }

    .label-area {
        display: flex;
        align-items: center;
    }

    .caret.hidden {
        display: none;
    }
    .caret {
        display: block;
        margin-left: auto;
        font-size: 1.1em;
    }
</style>
<div class="label-area">
    <span class="label"><slot></slot></span>
    <div class="caret hidden">→</div>
</div>
<div class="submenu-area">
    <slot name="submenu"></slot>
</div>
`;

class ContextMenuItem extends HTMLElement {
    constructor(){
        super();

        // Setup shadow dom and template
        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.append(
            this.template.content.cloneNode(true)
        );

        // Bound methods
        this.showCaret = this.showCaret.bind(this);
    }

    showCaret(){
        let caretEl = this._shadowRoot.querySelector('.caret');
        caretEl.classList.remove('hidden');    
    }
};

export {
    ContextMenuItem,
    ContextMenuItem as default
};


<|start_filename|>js/objects/tests/test-arithmetic-with-preload.js<|end_filename|>
import chai from 'chai';
import ohm from 'ohm-js';
const assert = chai.assert;
const expect = chai.expect;

import interpreterSemantics from '../../ohm/interpreter-semantics.js';

let testLanguageGrammar = ohm.grammar(window.grammar);

describe("Arithmetic Interpreter Tests", () => {
    let semantics;
    before(() => {
        semantics = testLanguageGrammar.createSemantics();
        semantics.addOperation(
            'interpret',
            interpreterSemantics(null, window.System)
        );
    });

    it("Can add", () => {
        let input = "5 + 5";
        let expected = "10";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });
    it("Can add with parantheses", () => {
        let input = "(5 + 5)";
        let expected = "10";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });
    it("Can subtract", () => {
        let input = "5 - 5";
        let expected = "0";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });
    it("Can subtract with parantheses", () => {
        let input = "(5 - 5)";
        let expected = "0";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });
    it("Can divide", () => {
        let input = "6 / 5";
        let expected = "1.2";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });
    it("Can divide with parantheses", () => {
        let input = "(6 / 5)";
        let expected = "1.2";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });
    it("Can modulo divide", () => {
        let input = "5 % 5";
        let expected = "0";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });
    it("Can modulo divide with parantheses", () => {
        let input = "(5 % 5)";
        let expected = "0";
        let matchObject = testLanguageGrammar.match(input, "Expression");
        assert.isTrue(matchObject.succeeded());
        let result = semantics(matchObject).interpret();
        assert.equal(expected, result);
    });

});


<|start_filename|>js/ohm/tests/test-tell-grammar.js<|end_filename|>
/** Test grammar of the new 'tell' command **/
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
};
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};

const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
};

describe('#tell command tests', () => {
    describe("Simple ObjectSpecifiers", () => {
        it("Can tell this card to perform some arbitrary command with args", () => {
            let str = `tell this card to myCustomCommand 3, "hello", -0.5`;
            semanticMatchTest(str, 'Command_tellCommand');
        });
        it("Can tell a target to perform some arbitrary command with args", () => {
            let str = `tell target to myCustomCommand 3, "hello", -0.5`;
            semanticMatchTest(str, 'Command_tellCommand');
        });
        it("Can tell a nested specifier to perform an arbitrary command", () => {
            let str = `tell second button of card 5 of stack "myStack" to myCustomCommand 3, "hello", -0.3`;
            semanticMatchTest(str, 'Command_tellCommand');
        });
    });
});


<|start_filename|>plugins/merriam.js<|end_filename|>
const merriam = {
    name: "MerriamAPI",
    load: function(url){
        this.src = url;
        return true;
    },
    response: null,
    cache: null,
    cacheNextItem: null,
    currentResponseItemIndex: -1,
    src: null,
    get: async function(prerequisite, arg){
        if(arg){
            if(!this.cache){
                throw Error("No result has been fetched");
            }
            if(arg == "next"){
                let results = this.cache.results;
                this.currentResponseItemIndex += 1;
                if(this.currentResponseItemIndex >= results.length){
                    this.currentResponseItemIndex = 0;
                }
                this.response = this.cache.results[this.currentResponseItemIndex];
                // put the result in the next item cache
                this.cacheNextItem = this.cache.results[this.currentResponseItemIndex];
                return JSON.stringify(this.response, null, '\t');
            }
            // here we assume that the arg is a key in the next item cache
            this.response = this.cacheNextItem[arg];
            return this.response;
        }
        const payload = parsePrerequisite(prerequisite);
        const params = {
            "method": "POST",
            "headers": {
                "content-type": "application/json"
            },
            "body": JSON.stringify(payload)
        };
        let response = await fetch(this.src, params);
        if (response.ok) { // if HTTP-status is 200-299
            // get the response body (the method explained below)
            let json = await response.json();
            this.response = json;
            this.cache = json;
            return JSON.stringify(this.response, null, '\t');
        } else {
            console.error("HTTP-Error: " + response.status);
            return false;
        };
    }
}


let parsePrerequisite = (p) => {
    let pList = p.split(";");
    let payload = {};
    pList.forEach((item) => {
        let key = item.split(":")[0];
        let value = item.replace(`${key}:`, "");
        switch(key){
        case "fields":
            payload["fields"] = value.split(',');
            break;
        case "weights":
            let weights = {};
            value.split(",").forEach((w) => {
                let [w_name, w_value] = w.split(":");
                weights[w_name] = Number(w_value);
            });
            payload["weights"] = weights;
            break;
        case "doc_ids":
            payload["doc_ids"] = value.split(",");
            break;
        case "limit":
            payload["limit"] = Number(value);
            break;
        }
    });
    return payload;
}


export {
    merriam,
    merriam as default
};


<|start_filename|>js/objects/tests/test-repeat-with-preload.js<|end_filename|>
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

let currentCardModel;
let buttonModel;

function compileButtonScript(aScript){
    let msg = {
        type: 'compile',
        codeString: aScript,
        targetId: buttonModel.id
    };
    window.System.compile(msg);
};

function sendButtonClick(){
    let clickMessage = {
        type: 'command',
        commandName: 'click',
        args: []
    };
    buttonModel.sendMessage(clickMessage, buttonModel);
};

function getLocalVar(obj, varName){
    return obj._executionContext.getLocal(varName);
};

describe.skip("Repeat Looping Tests", () => {
    describe("Model Setup", () => {
        it('Can find the current card model', () => {
            let currentCardView = document.querySelector('.current-stack > .current-card');
            currentCardModel = currentCardView.model;
            assert.exists(currentCardModel);
        });
        it('Can add a button to current card model without error', () => {
            let addButton = function(){
                let msg = {
                    type: 'command',
                    commandName: 'newModel',
                    args: [
                        'button',
                        currentCardModel.id,
                        'card'
                    ]
                };
                currentCardModel.sendMessage(msg, currentCardModel);
            };
            expect(addButton).to.not.throw(Error);
        });
        it('Can find newly created button model', () => {
            let button = currentCardModel.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            buttonModel = button;
            assert.exists(buttonModel);
        });
    });

    describe("Repeat forNumTimes tests", () => {
            it("Can compile a basic script and loop correct num times", () => {
                let script = [
                    "on click",
                    "put 0 into myNum",
                    "repeat for 5 times",
                    "put (myNum + 1) into myNum",
                    "end repeat",
                    "end click"
                ].join("\n");
                compileButtonScript(script);
                sendButtonClick(script);
                let result = getLocalVar(buttonModel, "myNum");
                assert.equal(result, 5);
            });
            it("Can compile a script and loop num times based on variable", () => {
                let script = [
                    "on click",
                    "put 0 into myNum",
                    "put 3 into myCount",
                    "repeat for myCount times",
                    "put (myNum + 1) into myNum",
                    "end repeat",
                    "end click"
                ].join("\n");
                compileButtonScript(script);
                sendButtonClick(script);
                let result = getLocalVar(buttonModel, "myNum");
                assert.equal(result, 3);
            });
    });
    describe("Repeat untilCondition tests", () => {
        it("Can compile and loop until condition is met", () => {
            let script = [
                "on click",
                "\tput 5 into myLimit",
                "\tput 0 into myCount",
                "\trepeat until myLimit <= 0",
                "\tput (myCount + 1) into myCount",
                "\tput (myLimit - 1) into myLimit",
                "\tend repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick(script);
            let resultLimit = getLocalVar(buttonModel, "myLimit");
            let resultCount = getLocalVar(buttonModel, "myCount");
            assert.equal(resultLimit, 0);
            assert.equal(resultCount, 5);
        });
    });

    describe("Repeat whileCondition tests", () => {
        it("Can compile and loop while condition is true", () => {
            let script = [
                "on click",
                "\tput 0 into myLimit",
                "\tput 0 into myCount",
                "\trepeat while myLimit < 6 ",
                "\tput (myCount + 1) into myCount",
                "\tput (myLimit + 1) into myLimit",
                "\tend repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick(script);
            let resultLimit = getLocalVar(buttonModel, "myLimit");
            let resultCount = getLocalVar(buttonModel, "myCount");
            assert.equal(resultLimit, 6);
            assert.equal(resultCount, 6);
        });
    });

    describe("Repeat withStartFinish tests", () => {
        it("Can compile and run when range is valid", () => {
            let script = [
                "on click",
                "put 0 into myTotal",
                "put 0 into myCount",
                "repeat with realCount = 1 to 3",
                "put (realCount + myTotal) into myTotal",
                "put (myCount + 1) into myCount",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let resultTotal = getLocalVar(buttonModel, 'myTotal');
            let resultCount = getLocalVar(buttonModel, "myCount");
            assert.equal(resultCount, 3);
            assert.equal(resultTotal, 6);
        });
    });

    describe("Misc example tests", () => {
        it("Can exit repeat on if condition (until)", () => {
            let script = [
                "on click",
                "put 0 into Counter",
                "repeat until Counter == 5",
                "answer Counter",
                "put (Counter + 1) into Counter",
                "if Counter >= 3 then exit repeat",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let result = getLocalVar(buttonModel, 'Counter');
            assert.equal(result, 3);
        });

        it("Can exit repeat on if condition (forNumTimes)", () => {
            let script = [
                "on click",
                "put 0 into Counter",
                "repeat 5 times",
                "if Counter == 4 then exit repeat",
                "put (Counter + 1) into Counter",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let result = getLocalVar(buttonModel, 'Counter');
            assert.equal(result, 4);
        });

        it("Can exit repeat on if condition (while)", () => {
            let script = [
                "on click",
                "put 0 into Counter",
                "put 0 into timesItLooped",
                "repeat while Counter < 6",
                "put (Counter + 1) into Counter",
                "if Counter > 3 then exit repeat",
                "put (timesItLooped + 1) into timesItLooped",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let counter = getLocalVar(buttonModel, 'Counter');
            let timesItLooped = getLocalVar(buttonModel, 'timesItLooped');
            assert.equal(counter, 4);
            assert.equal(timesItLooped, 3);
        });

        it("Can exit repeat on if condition (withStartFinish)", () => {
            let script = [
                "on click",
                "put 0 into Counter",
                "repeat with realCount = 2 to 7",
                "if Counter > 2 then exit repeat",
                "put (Counter + 1) into Counter",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let counter = getLocalVar(buttonModel, 'Counter');
            let finalIndex = getLocalVar(buttonModel, 'realCount');
            assert.equal(counter, 3);
            assert.equal(finalIndex, 5);
        });

        it("Can next repeat on if condition (until)", () => {
            let script = [
                "on click",
                "put 0 into Counter",
                "put 0 into CounterMinusOne",
                "repeat until Counter == 5",
                "answer Counter",
                "put (Counter + 1) into Counter",
                "if Counter == 3 then next repeat",
                "put (CounterMinusOne + 1) into CounterMinusOne",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let result = getLocalVar(buttonModel, 'Counter');
            let backupResult = getLocalVar(buttonModel, 'CounterMinusOne');
            assert.equal(result, 5);
            assert.equal(backupResult, 4);
        });

        it("Can next repeat on if condition (forNumTimes)", () => {
            let script = [
                "on click",
                "put 0 into Counter",
                "repeat for 10 times",
                "if Counter > 3 then next repeat",
                "put (Counter + 1) into Counter",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let result = getLocalVar(buttonModel, 'Counter');
            assert.equal(result, 4);
        });

        it("Can next repeat on if condition (while)", () => {
            let script = [
                "on click",
                "put 0 into Counter",
                "put 0 into timesItLooped",
                "repeat while Counter < 6",
                "put (Counter + 1) into Counter",
                "if Counter == 3 then next repeat",
                "put (timesItLooped + 1) into timesItLooped",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let counter = getLocalVar(buttonModel, 'Counter');
            let timesItLooped = getLocalVar(buttonModel, 'timesItLooped');
            assert.equal(counter, 6);
            assert.equal(timesItLooped, 5);
        });

        it("Can next repeat on if condition (withStartFinish)" , () => {
            let script = [
                "on click",
                "put 0 into Counter",
                "repeat with realCount = 2 to 7",
                "if realCount == 4 then next repeat",
                "put (Counter + 1) into Counter",
                "end repeat",
                "end click"
            ].join("\n");
            compileButtonScript(script);
            sendButtonClick();
            let counter = getLocalVar(buttonModel, "Counter");
            assert.equal(counter, 4);
        });        
    });
});






<|start_filename|>js/objects/views/Halo.js<|end_filename|>
/**
 * New Halo
 */

/** Note: Icons are from 
*** https://tablericons.com/
**/
const deleteIcon =`
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <line x1="4" y1="7" x2="20" y2="7" />
  <line x1="10" y1="11" x2="10" y2="17" />
  <line x1="14" y1="11" x2="14" y2="17" />
  <path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
  <path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</svg>
`;
const editIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-edit" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M9 7h-3a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-3" />
  <path d="M9 15h3l8.5 -8.5a1.5 1.5 0 0 0 -3 -3l-8.5 8.5v3" />
  <line x1="16" y1="5" x2="19" y2="8" />
</svg>
`;
const growIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-diagonal-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <polyline points="16 20 20 20 20 16" />
  <line x1="14" y1="14" x2="20" y2="20" />
  <polyline points="8 4 4 4 4 8" />
  <line x1="4" y1="4" x2="10" y2="10" />
</svg>
`;

const copyIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copy" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
   <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
   <rect x="8" y="8" width="12" height="12" rx="2"></rect>
   <path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path>
</svg>
`;

const pasteIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
   <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
   <path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2"></path>
   <rect x="9" y="3" width="6" height="4" rx="2"></rect>
   <path d="M9 14l2 2l4 -4"></path>
</svg>
`;

const targetIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-focus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
   <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
   <circle cx="12" cy="12" r=".5" fill="currentColor"></circle>
   <circle cx="12" cy="12" r="9"></circle>
</svg>
`;

const settingsIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-settings" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
  <circle cx="12" cy="12" r="3" />
</svg>
`;

const rotateIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rotate-clockwise" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
   <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
   <path d="M4.05 11a8 8 0 1 1 .5 4m-.5 5v-5h5"></path>
</svg>
`;

const templateString = `
<style>
 :host {
     --halo-button-height: 25px;
     --halo-button-width: 25px;
     --halo-rim-margin: 10px;
     --halo-button-width-padded: calc(var(--halo-button-width) + var(--halo-rim-margin));
     --halo-button-height-padded: calc(var(--halo-button-height) + var(--halo-rim-margin));
     position: absolute;
     box-sizing: border-box;
     top: calc(-1 * var(--halo-button-height-padded));
     left: calc(-1 * var(--halo-button-width-padded));
     width: calc(100% + (2 * var(--halo-button-width-padded)));
     height: calc(100% + (2 * var(--halo-button-height-padded)));
     color: initial;
     z-index: 10;
 }


 .halo-row,
 .halo-column {
     display: flex;
     position: absolute;
 }

 .halo-column {
     flex-direction: column;
 }

 #halo-top-row,
 #halo-bottom-row {
     width: calc(100% - var(--halo-button-width-padded));
     height: var(--halo-button-height-padded);
 }

 #halo-top-row {
     left: 0;
     top: 0;
 }

 #halo-bottom-row {
     right: 0;
     bottom: 0;
     flex-direction: row-reverse;
     align-items: flex-end;
 }

 #halo-right-column,
 #halo-left-column {
     height: calc(100% - var(--halo-button-height-padded));
     width: var(--halo-button-width-padded);
 }

 #halo-right-column {
     right: 0;
     top: 0;
     align-items: flex-end;
 }

 #halo-left-column {
     left: 0;
     top: var(--halo-button-height-padded);
 }

 .halo-button,
 ::slotted(*) {
     display: block;
     border: 1px solid rgba(100, 100, 100, 0.8);
     width: var(--halo-button-width);
     height: var(--halo-button-height);
     background-color: rgb(220, 220, 220);
 }

 .halo-button:hover
 ::slotted(*).halo-button:hover {
     cursor: pointer;
 }

 .halo-button:active
 ::slotted(*).halo-button:active {
     border: 1px solid black;
 }

 .halo-button.hidden
 ::slotted(*).halo-button.hidden {
     display: none;
 }

</style>

<div id="halo-top-row" class="halo-row">
    <div id="halo-delete" class="halo-button" title="Delete this part">
        ${deleteIcon}
    </div>
    <slot name="top-row"></slot>
</div>

<div id="halo-bottom-row" class="halo-row">
    <div id="halo-resize" class="halo-button" title="Resize this part">
        ${growIcon}
    </div>
    <div id="halo-script-edit" class="halo-button" title="Edit this part's script">
        ${editIcon}
    </div>
    <div id="halo-edit" class="halo-button" title="Edit this part">
        ${settingsIcon}
    </div>
    <slot name="bottom-row"></slot>
</div>

<div id="halo-left-column" class="halo-column">
    <div id="halo-copy" class="halo-button" title="Copy this Part">
        ${copyIcon}
    </div>
    <div id="halo-paste" class="halo-button" title="Paste the contents of clipboard into this Part">
        ${pasteIcon}
    </div>
    <div id="halo-target" class="halo-button" title="Select this Part's target">
        ${targetIcon}
    </div>
    <slot name="left-column"></slot>
</div>

<div id="halo-right-column" class="halo-column">
    <div id="halo-rotate" class="halo-button" title="Rotate this part">
        ${rotateIcon}
    </div>
    <slot name="right-column"></slot>
</div>

`;

class Halo extends HTMLElement {
    constructor(){
        super();

        // Configure the Shadow DOM and template
        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this.shadow = this.attachShadow({mode: 'open'});
        this.shadow.append(
            this.template.content.cloneNode(true)
        );

        // Bind component methods


        // Bind event listeners
        this.onMouseDown = this.onMouseDown.bind(this);
        this.onMouseUp = this.onMouseUp.bind(this);
        this.onMouseMove = this.onMouseMove.bind(this);
        this.onResizeMouseDown = this.onResizeMouseDown.bind(this);
        this.onResizeMouseUp = this.onResizeMouseUp.bind(this);
        this.onResizeMouseMove = this.onResizeMouseMove.bind(this);
        this.onRotateMouseDown = this.onRotateMouseDown.bind(this);
        this.onRotateMouseUp = this.onRotateMouseUp.bind(this);
        this.onRotateMouseMove = this.onRotateMouseMove.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.targetElement = this.getRootNode().host;
            this.targetElement.classList.add('editing');
            this.targetElement.hasOpenHalo = true;

            // Add event listeners
            this.addEventListener('mousedown', this.onMouseDown);

            // Resize button
            this.resizer = this.shadowRoot.getElementById('halo-resize');
            this.resizer.addEventListener('mousedown', this.onResizeMouseDown);
            if(!this.targetElement.wantsHaloResize){
                this.resizer.style.visibility = 'hidden';
            }

            // Rotate button
            this.rotater = this.shadowRoot.getElementById('halo-rotate');
            this.rotater.addEventListener('mousedown', this.onRotateMouseDown);
            if(!this.targetElement.wantsHaloRotate){
                this.rotater.style.visibility = 'hidden';
            }
            // Delete button
            this.deleter = this.shadowRoot.getElementById('halo-delete');
            this.deleter.addEventListener('click', this.targetElement.onHaloDelete);
            if(!this.targetElement.wantsHaloDelete){
                this.deleter.style.visibility = 'hidden';
            }

            // Edit button
            this.scriptEditor = this.shadowRoot.getElementById('halo-script-edit');
            this.scriptEditor.addEventListener('click', this.targetElement.onHaloOpenScriptEditor);
            if(!this.targetElement.wantsHaloScriptEdit){
                this.scriptEditor.style.visibility = 'hidden';
            }

            // Comprehensive editor button
            this.editor = this.shadowRoot.getElementById('halo-edit');
            this.editor.addEventListener('click', this.targetElement.onHaloOpenEditor);
            if(!this.targetElement.wantsHaloEdit){
                this.editor.style.visibility = 'hidden';
            }

            // Copy button
            this.copier = this.shadowRoot.getElementById('halo-copy');
            this.copier.addEventListener('click', this.targetElement.onHaloCopy);


            // Paste button
            this.paster = this.shadowRoot.getElementById('halo-paste');
            this.paster.addEventListener('click', this.targetElement.onHaloPaste);

            // Target button
            this.targeter = this.shadowRoot.getElementById('halo-target');
            this.targeter.addEventListener('click', this.targetElement.onHaloTarget);
            this.targeter.addEventListener('mouseenter', this.targetElement.onHaloTargetButtonMouseEnter);
            this.targeter.addEventListener('mouseleave', this.targetElement.onHaloTargetButtonMouseLeave);
        }
    }

    disconnectedCallback(){
        this.targetElement.classList.remove('editing');
        this.targetElement.hasOpenHalo = false;

        // Remove event listeners
        this.removeEventListener('mousedown', this.onMouseDown);
        this.resizer.removeEventListener('mousedown', this.onResizeMouseDown);
    }


    /* Event Handling */
    onMouseDown(event){
        if(event.button == 0 && this.targetElement.wantsHaloMove){
            document.addEventListener('mousemove', this.onMouseMove);
            document.addEventListener('mouseup', this.onMouseUp);
        }
    }

    onMouseMove(event){
        let currentTop = parseInt(this.targetElement.style.top);
        let currentLeft = parseInt(this.targetElement.style.left);
        let newTop = event.movementY + currentTop;
        let newLeft = event.movementX + currentLeft;

        let model = this.targetElement.model;
        model.partProperties.setPropertyNamed(model, "top", newTop);
        model.partProperties.setPropertyNamed(model, "left", newLeft);
    }

    onMouseUp(event){
        document.removeEventListener('mousemove', this.onMouseMove);
        document.removeEventListener('mouseup', this.onMouseUp);
    }

    onResizeMouseDown(event){
        event.stopPropagation();
        document.addEventListener('mousemove', this.onResizeMouseMove);
        document.addEventListener('mouseup', this.onResizeMouseUp);
    }

    onResizeMouseUp(event){
        document.removeEventListener('mousemove', this.onResizeMouseMove);
        document.removeEventListener('mouseup', this.onResizeMouseUp);
    }

    onResizeMouseMove(event){
        this.targetElement.onHaloResize(
            event.movementX,
            event.movementY
        );
    }

    onRotateMouseDown(event){
        event.stopPropagation();
        document.addEventListener('mousemove', this.onRotateMouseMove);
        document.addEventListener('mouseup', this.onRotateMouseUp);
    }

    onRotateMouseUp(event){
        document.removeEventListener('mousemove', this.onRotateMouseMove);
        document.removeEventListener('mouseup', this.onRotateMouseUp);
    }

    onRotateMouseMove(event){
        this.targetElement.onHaloRotate(
            event.movementX,
            event.movementY
        );
    }
};

export {
    Halo,
    Halo as default
};


<|start_filename|>js/objects/tests/preload.js<|end_filename|>
/**
 * WC Testing Preload
 * ----------------------------------
 * This preload file creates a JSDOM instance
 * and sets some global variables that other
 * modules (like System) will expect at the global scope,
 * like window or HTMLElement.
 * This file is not a test but should be required when
 * launching mocha via the require flag
 */
var jsdom = require('jsdom');

global.window = new jsdom.JSDOM().window;
global.document = window.document;
global.HTMLElement = window.HTMLElement;
global.fetch = require("node-fetch");
global.DOMParser = window.DOMParser;
global.CustomEvent = window.CustomEvent;
global.alert = function alert(arg) {};
document.execCommand = function execCommandMock() { };


// add the grammar in
var fs = require('fs');
window.grammar = fs.readFileSync('./js/ohm/simpletalk.ohm');

// Initialize the system
const System = require('../System.js').System;
global.System = System;
global.window.System = System;
// NOTE: do not call initialLoad() or
// registerCustomElements() here.
// Because System attaches a listener
// for DOMContentLoaded to the document
// object -- which here is already defined
// in jsdom -- it will call those automatically.
// Calling them twice will produce errors!


<|start_filename|>js/objects/utils/styleProperties.js<|end_filename|>
/**
 * Helpers for setting up various
 * style properties
 */


/**
 * Basic style properties are those
 * common to all (visual) Parts
 */
const sides = ["top", "bottom", "left", "right"];

const addBasicStyleProps = (target) => {
    target.partProperties.newStyleProp(
        'background-transparency',
        1,
    );
    target.partProperties.newStyleProp(
        'background-color',
        "rgb(255, 255, 255)", // white 
    );
    target.partProperties.newStyleProp(
        'transparency',
        1.0,
    );
    sides.forEach((s) => {
        target.partProperties.newStyleProp(
            `border-${s}-style`,
            'solid'
        );
    });
    sides.forEach((s) => {
        target.partProperties.newStyleProp(
            `border-${s}-width`,
            0,
        );
    });
    sides.forEach((s) => {
        target.partProperties.newStyleProp(
            `border-${s}-color`,
            "rgb(0, 0, 0)", // black
        );
    });
    sides.forEach((s) => {
        target.partProperties.newStyleProp(
            `border-${s}-transparency`,
            1
        );
    });
    target.partProperties.newStyleProp(
        'shadow-left',
        0
    );
    target.partProperties.newStyleProp(
        'shadow-top',
        0
    );
    target.partProperties.newStyleProp(
        'shadow-blur',
        0
    );
    target.partProperties.newStyleProp(
        'shadow-spread',
        0
    );
    target.partProperties.newStyleProp(
        'shadow-color',
        "rgb(238, 238, 238)", // grey
    );
    target.partProperties.newStyleProp(
        'shadow-transparency',
        1
    );
    target.partProperties.newStyleProp(
        'corner-top-left-round',
        0
    );
    target.partProperties.newStyleProp(
        'corner-top-right-round',
        0
    );
    target.partProperties.newStyleProp(
        'corner-bottom-left-round',
        0
    );
    target.partProperties.newStyleProp(
        'corner-bottom-right-round',
        0
    );
};

/**
 * Style properties for Parts that can
 * be moved and that can have explicit
 * dimensions. Examples: buttons, fields.
 * Examples of those that can't: Cards, Stacks
 */
const addPositioningStyleProps = (target) => {
    target.partProperties.newStyleProp(
        'hide',
        false,
    );
    // setting width and height to null
    // effectively forces to the default size
    // of the button to fit the button name
    target.partProperties.newStyleProp(
        'width',
        100,
    );
    target.partProperties.newStyleProp(
        'height',
        null,
    );
    target.partProperties.newStyleProp(
        'top',
        0,
    );
    target.partProperties.newStyleProp(
        'left',
        0,
    );
    target.partProperties.newStyleProp(
        'rotate',
        null,
    );

    // horizontal-resizing specifies a strategy
    // for how this Part should adjust its
    // horizontal axis relative to the parent.
    // Possible values are:
    // * rigid - Stick to the top, left, width
    // and height properties as they are explicitly
    // set;
    // * shrink-wrap - Become large enough on the hori-
    // zontal axis only to fit any child contents;
    // * space-fill - Fill as much as we can in the
    // horizontal axis in the parent Part.
    target.partProperties.newStyleProp(
        'horizontal-resizing',
        'rigid'
    );

    // vertical-resizing specifies a strategy
    // for how this Part should adjust its
    // vertical axis relative to the parent.
    // Possible values are:
    // * rigid - Stick to the top, left, width
    // and height properties as they are explicitly
    // set;
    // * shrink-wrap - Become large enough on the hori-
    // zontal axis only to fit any child contents;
    // * space-fill - Fill as much as we can in the
    // vertical axis in the parent Part.
    target.partProperties.newStyleProp(
        'vertical-resizing',
        'rigid'
    );

    // Margin specifies some space between the
    // target Part and any other Parts that might
    // be adjacent to it in a common Owner. It will
    // not be in effect when the owner is using a
    // strict layout.
    target.partProperties.newStyleProp(
        'top-margin',
        null
    );
    target.partProperties.newStyleProp(
        'right-margin',
        null
    );
    target.partProperties.newStyleProp(
        'bottom-margin',
        null
    );
    target.partProperties.newStyleProp(
        'left-margin',
        null
    );

    // Pinning specifies whether or not
    // a given part should "stick" to a
    // particular side of its owner Part.
    // Pinning properties only have effect
    // inside of Parts with a strict layout
    target.partProperties.newDynamicProp(
        'pinning-top',
        pinningSetter,
        function(propOwner, propObject){
            return propObject._value;
        },
        false, // not read only
        false // default value

    );
    target.partProperties.newDynamicProp(
        'pinning-left',
        pinningSetter,
        function(propOwner, propObject){
            return propObject._value;
        },
        false, // not read only
        false // default value

    );
    target.partProperties.newDynamicProp(
        'pinning-bottom',
        pinningSetter,
        function(propOwner, propObject){
            return propObject._value;
        },
        false, // not read only
        false // default value
    );
    target.partProperties.newDynamicProp(
        'pinning-right',
        pinningSetter,
        function(propOwner, propObject){
            return propObject._value;
        },
        false, // not read only
        false // default value
    );
    target.partProperties.newDynamicProp(
        // Possible values for the compound
        // 'pinning' property are:
        // *"none" or null
        // *top
        // *top-right
        // *top-left
        // *bottom
        // *bottom-right
        // *bottom-left
        // *left
        // *right
        'pinning',
        // Setter
        function(propOwner, propObject, value){
            if(!value || value == "none"){
                ['top', 'left', 'right', 'bottom'].forEach(side => {
                    let pin = `pinning-${side}`;
                    propOwner.partProperties.setPropertyNamed(
                        propOwner,
                        pin,
                        false
                    );
                });
                return;
            }
            pinningAdjust(propOwner, value);
        },

        // Getter
        function(propOwner, propObject){
            let top = propOwner.partProperties.getPropertyNamed(
                propOwner,
                'pinning-top'
            );
            let bottom = propOwner.partProperties.getPropertyNamed(
                propOwner,
                'pinning-bottom'
            );
            let left = propOwner.partProperties.getPropertyNamed(
                propOwner,
                'pinning-left'
            );
            let right = propOwner.partProperties.getPropertyNamed(
                propOwner,
                'pinning-right'
            );
            let result = [];
            if(top){
                result.push('top');
            } else if(bottom){
                result.push('bottom');
            }
            if(left){
                result.push('left');
            } else if(right){
                result.push('right');
            }

            return result.join('-');
        }
    );
};

/**
 * Style properties for Parts that display
 * text
 */
const addTextStyleProps = (target) => {
    target.partProperties.newStyleProp(
        'text-align',
        'left',
        'cssTextStyle'
    );
    target.partProperties.newStyleProp(
        'text-font',
        'default',
        'cssTextStyle'
    );
    target.partProperties.newStyleProp(
        'text-color',
        "rgb(0, 0, 0)", // black
        'cssTextStyle'
    );
    target.partProperties.newStyleProp(
        'text-transparency',
        1,
        'cssTextStyle'
    );
    target.partProperties.newStyleProp(
        'text-style',
        'plain',
        'cssTextStyle'
    );
    target.partProperties.newStyleProp(
        'text-bold',
        false,
        'cssTextStyle'
    );
    target.partProperties.newStyleProp(
        'text-italic',
        false,
        'cssTextStyle'
    );
    target.partProperties.newStyleProp(
        'text-size',
        15,
        'cssTextStyle'
    );
};

/**
 * Basic layout styles are those pertaining
 * to the positioning and resizing of subparts.
 * Examples include Cards and Area
 */
const addLayoutStyleProps = (target) => {
    // The 'layout' property is
    // one of two strings:
    // strict - Equivalent to the absolute
    // layout based strictly on coordinates
    // list - Will force items into either a row
    // or column list, based on the pairing with
    // the 'listDirection' property
    target.partProperties.newBasicProp(
        'layout',
        'strict'
    );

    // list-direction specifies row or column
    // and will only have an effect whent the
    // layout property is set to 'list'
    target.partProperties.newBasicProp(
        'list-direction',
        'row'
    );

    // Wrapping specifies whether a list should
    // wrap along its dominant dimension (row or column)
    target.partProperties.newBasicProp(
        'list-wrapping',
        false
    );

    // Padding specifies some space from the
    // border of the target Part to the beginning
    // of the layout of any subparts.
    target.partProperties.newStyleProp(
        'top-padding',
        null
    );
    target.partProperties.newStyleProp(
        'right-padding',
        null
    );
    target.partProperties.newStyleProp(
        'bottom-padding',
        null
    );
    target.partProperties.newStyleProp(
        'left-padding',
        null
    );

    // List alignment describes how elements in
    // a list layout should align themselves along
    // the dominant dimension (row or column)
    // They are essentially proxies for align-items
    target.partProperties.newBasicProp(
        'list-alignment',
        null
    );

    // List distribution describes how elements
    // in a list layout should distribute themselves
    // across or along the dominant dimension
    // (row or column)
    // This is essentially a wrapper for justify-content
    target.partProperties.newBasicProp(
        'list-distribution',
        null
    );
};

/**
  * HELPERS
 **/

const pinningSetter = (propOwner, propObject, value) => {
    let side = propObject.name.split("-")[1];
    let topLeft;
    switch (side){
    case "right":
        topLeft = "left";
        break;
    case "bottom":
        topLeft = "top";
        break;
    default:
        topLeft = side;
    }
    // we'll need to fix and un-fix the corresponding top or left property depending
    // on whether value is true of false, respectively
    let prop = propOwner.partProperties.findPropertyNamed(
        topLeft 
    );
    let oppositeSide;
    switch (side){
    case "left":
        oppositeSide = "right";
        break;
    case "right":
        oppositeSide = "left";
        break;
    case "top":
        oppositeSide = "bottom";
        break;
    case "bottom":
        oppositeSide = "top";
        break;
    }
    if(value){
        // first make sure that pinning-bottom is false
        propOwner.partProperties.setPropertyNamed(
            propOwner,
            `pinning-${oppositeSide}`,
            false
        );
        prop.readOnly = true;
    } else {
        // reset the value back to trigger a notification
        prop.setValue(propOwner, prop._value);
        prop.readOnly = false;
    }
    propObject._value = value;
};

const pinningAdjust = (owner, value) => {
    let sides = ['top', 'left', 'right', 'bottom'];
    sides.forEach(side => {
        if(value.startsWith(side)){
            owner.partProperties.setPropertyNamed(
                owner,
                `pinning-${side}`,
                true
            );
        } else {
            owner.partProperties.setPropertyNamed(
                owner,
                `pinning-${side}`,
                false
            );
        }
    });

    if(value.includes("-")){
        if(value.endsWith('left')){
            owner.partProperties.setPropertyNamed(
                owner,
                'pinning-left',
                true
            );
            owner.partProperties.setPropertyNamed(
                owner,
                'pinning-right',
                false
            );
        } else if(value.endsWith('right')){
            owner.partProperties.setPropertyNamed(
                owner,
                'pinning-left',
                false
            );
            owner.partProperties.setPropertyNamed(
                owner,
                'pinning-right',
                true
            );
        }
    }
};

export {
    addBasicStyleProps,
    addPositioningStyleProps,
    addTextStyleProps,
    addLayoutStyleProps
};


<|start_filename|>js/objects/tests/test-clipboard-with-preload.js<|end_filename|>
/**
 * STClipboard Related Tests
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

const expectedMimeType = "simpletalk/json";

describe("Basic Copy and Paste Tests", () => {
    let currentCard;
    let currentStack;
    let initialButton;
    let initialArea;
    describe("Model setup", () => {
        it("Can add a button and area to the initial card", () => {
            currentCard = window.System.getCurrentCardModel();
            assert.exists(currentCard);
            initialButton = window.System.newModel('button', currentCard.id, "Initial Button");
            assert.exists(initialButton);
            initialArea = window.System.newModel('area', currentCard.id, "Initial Area");
            assert.exists(initialArea);
        });
        it("The System clipboard is empty", () => {
            assert.isEmpty(window.System.clipboard.contents);
        });
        describe("Copying the initial button", () => {
            it("Places an item into the clipboard contents", () => {
                window.System.clipboard.copyPart(initialButton);
                assert.equal(window.System.clipboard.contents.length, 1);
            });
            it("Clipboard contents item has correct mimeType", () => {
                let item = window.System.clipboard.contents[0];
                assert.equal(item.type, expectedMimeType);
            });
            it("The encoded data, when deserialized, has the same type as the copied element", () => {
                let deserializedItem = JSON.parse(window.System.clipboard.contents[0].data);
                assert.equal(deserializedItem.type, "button");
            });
        });
        describe("Pasting the initial button into the area", () => {
            it("Can paste the clipboard contents (button) into the area on the card without error", (done) => {
                let pasteAction = function(){
                    window.System.clipboard.pasteContentsInto(initialArea).then(() => {
                        return done();
                    });
                };
                expect(pasteAction).to.not.throw();
            });
            it("The Area now has one subpart that is a button", () => {
                assert.equal(initialArea.subparts.length, 1);
                assert.equal(initialArea.subparts[0].type, "button");
            });
            it("The button in the area does *not* have the same id as the initial button", () => {
                let areaButton = initialArea.subparts.find(subpart =>  { return subpart.type == "button";});
                assert.notEqual(areaButton.id, initialButton.id);
            });
        });
        describe("Can copy the Area", () => {
            it("Places an item in to the clipboard contents", () => {
                window.System.clipboard.copyPart(initialArea);
                assert.equal(window.System.clipboard.contents.length, 1);
            });
            it("Clipboard contents item has correct mimeType", () => {
                let item = window.System.clipboard.contents[0];
                assert.equal(item.type, expectedMimeType);
            });
            it("The encoded data, when deserialized, has the same type as the copied element", () => {
                let deserializedItem = JSON.parse(window.System.clipboard.contents[0].data);
                assert.equal(deserializedItem.type, "area");
            });
            it("Can create and navigate to a new card", () => {
                currentStack = window.System.getCurrentStackModel();
                window.System.newModel('card', currentStack.id, "Second Card");
                window.System.goToNextCard();
                currentCard = window.System.getCurrentCardModel();
                let secondStackCard = currentStack.subparts.filter(subpart => {
                    return subpart.type == "card";
                })[1];
                assert.equal(
                    currentCard.id,
                    secondStackCard.id
                );
            });
        });
        describe("Can paste the area (into new card)", () => {
            it("The current card has no area subparts", () => {
                let areas = currentCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                });
                assert.equal(areas.length, 0);
            });
            it("Can paste the clipboard contents (area) into a new card without error", (done) => {
                let pasteAction = function(){
                    window.System.clipboard.pasteContentsInto(currentCard).then(() => {
                        done();
                    });
                };
                expect(pasteAction).to.not.throw();
            });
            it("The card now has one area subpart", () => {
                let areas = currentCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                });
                assert.equal(areas.length, 1);
            });
            it("The pasted area does *not* have the same id as the initial area", () => {
                let pastedArea = currentCard.subparts.find(subpart => { return subpart.type == 'area';});
                assert.notEqual(pastedArea.id, initialArea.id);
            });
            it("The pasted area has the name name as the initial area", () => {
                let pastedArea = currentCard.subparts.find(subpart => { return subpart.type == 'area';});
                let pastedName = pastedArea.partProperties.getPropertyNamed(
                    pastedArea,
                    'name'
                );
                let originalName = initialArea.partProperties.getPropertyNamed(
                    initialArea,
                    'name'
                );
                assert.equal(pastedName, originalName);
            });
        });
        describe("Can copy the current card", () => {
            it("Places an item into the clipboard contents", () => {
                window.System.clipboard.copyPart(currentCard);
                assert.equal(window.System.clipboard.contents.length, 1);
            });
            it("Clipboard contents item has correct mimeType", () => {
                let item = window.System.clipboard.contents[0];
                assert.equal(item.type, expectedMimeType);
            });
            it("The encoded data, when deserialized, has the same type as the copied element", () => {
                let deserializedItem = JSON.parse(window.System.clipboard.contents[0].data);
                assert.equal(deserializedItem.type, "card");
            });
        });
        describe("Can paste the current card into the current stack", () => {
            it("The current stack only has 2 cards to start", () => {
                let cards = currentStack.subparts.filter(subpart => {
                    return subpart.type == 'card';
                });
                assert.equal(cards.length, 2);
            });
            it("Can paste the clipboard contents (card) into the stack without error", (done) => {
                let pasteAction = function(){
                    window.System.clipboard.pasteContentsInto(currentStack).then(() => {
                        done();
                    });
                };
                expect(pasteAction).to.not.throw();
            });
            it("The current stack now has 3 cards", () => {
                let cards = currentStack.subparts.filter(subpart => {
                    return subpart.type == 'card';
                });
                assert.equal(cards.length, 3);
            });
            it("The pasted card does *not* have the same id as the copied card", () => {
                let cards = currentStack.subparts.filter(subpart => {
                    return subpart.type == 'card';
                });
                let pastedCard = cards[2];
                let copiedCard = cards[1];
                assert.notEqual(pastedCard.id, copiedCard.id);
            });
            it("The pasted card has an area", () => {
                let pastedCard = currentStack.subparts.filter(subpart => {
                    return subpart.type == 'card';
                })[2];
                let areas = pastedCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                });
                assert.equal(areas.length, 1);
            });
            it("The area in the pasted card has a button", () => {
                let pastedCard = currentStack.subparts.filter(subpart => {
                    return subpart.type == 'card';
                })[2];
                let area = pastedCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                })[0];
                let buttons = area.subparts.filter(subpart => {
                    return subpart.type == 'button';
                });
                assert.equal(buttons.length, 1);
            });
        });
    });
});


<|start_filename|>js/objects/views/ResourceView.js<|end_filename|>
import PartView from './PartView.js';

const linkIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5" />
  <path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5" />
</svg>
`;

const templateString = `
<style>
:host {
    box-sizing: border-box;
    display: block;
    position: absolute;
    padding: 1px;
    user-select: none;
}

.wrapper{
    width: 100%;
    height: 100%;
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}
}
</style>
<div class="wrapper">
    <span class="name"></span>
    <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-building-bridge-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
    <path d="M6 7h12a2 2 0 0 1 2 2v9a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a4 4 0 0 0 -8 0v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-9a2 2 0 0 1 2 -2"></path>
    </svg>
</div>
`;

class ResourceView extends PartView {
    constructor(){
        super();

        // Setup template and shadow dom
        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(this.template.content.cloneNode(true));

        // Bind component methods
        this.onClick = this.onClick.bind(this);
        this.initCustomHaloButton = this.initCustomHaloButton.bind(this);
        this.updateResourceLink = this.updateResourceLink.bind(this);
        this.indicateReadyState = this.indicateReadyState.bind(this);
    }

    afterConnected(){
        if(!this.haloButton){
            this.initCustomHaloButton();
        }
    }

    afterDisconnected(){
    }

    afterModelSet(){
        // if the resourceName property is set then make sure it is loaded
        // TODO: i don't like this view asking the model to load!
        let resourceName = this.model.partProperties.getPropertyNamed(this.model, "resourceName");
        if(resourceName){
            this.model.loadResource([this], resourceName);
        }
        let src = this.model.partProperties.getPropertyNamed(this.model, "src");
        if(src){
            this.model.setSourceTo([this], src);
        }
        let nameSpan = this._shadowRoot.querySelector(".name");
        nameSpan.innerText = this.model.partProperties.getPropertyNamed(this.model, "name");
        let state = this.model.partProperties.getPropertyNamed(
            this.model,
            "readyState",
        );
        this.indicateReadyState(state);
        // prop changes
        this.onPropChange("name", (value) => {
            nameSpan.innerText = value;
        });
        this.onPropChange("readyState", this.indicateReadyState);
        this.onPropChange("src", (url) => {
        });
    }

    onClick(event){
        if(event.button == 0){
            if(event.shiftKey){
                // prevent triggering the on click message
                event.preventDefault();
                if(this.hasOpenHalo){
                    this.model.partProperties.setPropertyNamed(this.model, "halo-open", false);
                } else {
                    this.model.partProperties.setPropertyNamed(this.model, "halo-open", true);
                }
            } else if(!this.hasOpenHalo){
                // Send the click command message to self
                this.model.sendMessage({
                    type: 'command',
                    commandName: 'click',
                    args: [],
                    shouldIgnore: true // Should ignore if System DNU
                }, this.model);
            }
        }
    }

    indicateReadyState(value){
        let borderColor = "red";
        if(value == "fetching"){
            borderColor = "yellow";
        } else if(value == "ready"){
            borderColor = "green";
        };
        ["right", "left", "top", "bottom"].forEach((side) => {
            this.model.partProperties.setPropertyNamed(this.model, `border-${side}-color`, borderColor);
        });
    }

    openHalo(){
        // Override default. Here we add a custom button
        // when showing.
        let foundHalo = this.shadowRoot.querySelector('st-halo');
        if(!foundHalo){
            foundHalo = document.createElement('st-halo');
            this.shadowRoot.appendChild(foundHalo);
        }
        foundHalo.append(this.haloButton);
    }

    initCustomHaloButton(){
        this.haloButton = document.createElement('div');
        this.haloButton.id = 'halo-resource-link';
        this.haloButton.classList.add('halo-button');
        this.haloButton.innerHTML = linkIcon;
        this.haloButton.style.marginTop = "6px";
        this.haloButton.setAttribute('slot', 'right-column');
        this.haloButton.setAttribute('title', 'Edit link for resource');
        this.haloButton.addEventListener('click', this.updateResourceLink);
    }

    updateResourceLink(event){
        // Tells the model to update its
        // src link for the resource
        let currentSrc = this.model.partProperties.getPropertyNamed(
            this.model,
            'src'
        );
        let result = window.prompt("Edit URL for resource:", currentSrc);
        if(result && result !== '' && result !== currentSrc){
            this.sendMessage(
                {
                    type: 'command',
                    commandName: 'loadResource',
                    args: [ result ]
                },
                this.model
            );
        }
    }


};

export {
    ResourceView,
    ResourceView as default
};


<|start_filename|>js/objects/tests/test-variable-scripts-with-preload.js<|end_filename|>
/**
 * Testing Variables with Scripts
 * ---------------------------------------
 * This module tests compilation of various scripts
 * that deal with variable passing and evaluation.
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

let currentCardModel;
let buttonModel;
describe('Model Setup', () => {
    it('Can find current card model', () => {
        let currentCardView = document.querySelector('.current-stack > .current-card');
        currentCardModel = currentCardView.model;
        assert.exists(currentCardModel);
    });
    it('Can add a button to current card model without error', () => {
        let addButton = function(){
            let msg = {
                type: 'command',
                commandName: 'newModel',
                args: [
                    'button',
                    currentCardModel.id,
                    'card'
                ]
            };
            currentCardModel.sendMessage(msg, currentCardModel);
        };
        expect(addButton).to.not.throw(Error);
    });
    it('Can find the newly created button model', () => {
        let button = currentCardModel.subparts.filter(subpart => {
            return subpart.type == 'button';
        })[0];
        buttonModel = button;
        assert.exists(buttonModel);
    });
});
describe('Basic Set Local Variable', () => {
    let script = [
        'on click',
        `\tput "hello" into myVariable`,
        'end click'
    ].join('\n');

    it('Can compile the basic put/into command without issue', () => {
        let sendFunction = function(){
            let msg = {
                type: 'compile',
                codeString: script,
                targetId: buttonModel.id
            };
            window.System.compile(msg);
        };
        expect(sendFunction).to.not.throw(Error);
    });

    it('Throws an error if attempting to read an undefined variable', () => {
        let testScript = [
            "on click",
            "put (notYetDefined + 7) into myNewVariable",
            "end click"
        ].join("\n");
        let sendFunction = function(){
            let msg = {
                type: 'compile',
                codeString: script,
                targetId: buttonModel.id
            };
            window.System.compile(msg);
        };
        expect(sendFunction).to.not.throw(Error);
        let buttonView = document.querySelector(`[part-id="${buttonModel.id}"]`);
        assert.exists(buttonView);
        expect(buttonView.click).to.throw();
    });
});

describe('Can set property based on local variable', () => {
    let script = [
        'on click',
        `\tput "new name" into myVariable`,
        `\tset "name" to myVariable`,
        'end click'
    ].join("\n");

    before(() => {
        // Clear the variable context on the existing
        // buttonModel, just to be safe.
        buttonModel._executionContext = null;
    });
    
    it('Can compile variable setting and property setting without error', () => {
        let sendFunction = function(){
            let msg = {
                type: 'compile',
                codeString: script,
                targetId: buttonModel.id
            };
            window.System.compile(msg);
        };
        expect(sendFunction).to.not.throw(Error);
    });
    it('Button does not yet have the new name prop (not clicked)', () => {
        let buttonView = document.querySelector(`[part-id="${buttonModel.id}"]`);
        assert.exists(buttonView);
        let currentName = buttonModel.partProperties.getPropertyNamed(
            buttonModel,
            'name'
        );
        assert.notEqual(currentName, 'new name');
    });
    it('Clicking updates to the new name prop', () => {
        let buttonView = document.querySelector(`[part-id="${buttonModel.id}"]`);
        buttonView.click();
        let foundName = buttonModel.partProperties.getPropertyNamed(
            buttonModel,
            'name'
        );
        assert.equal(foundName, 'new name');
    });
});


<|start_filename|>js/objects/views/ImageView.js<|end_filename|>
import PartView from './PartView.js';

const linkIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5" />
  <path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5" />
</svg>
`;

const pictureIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-photo" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
   <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
   <line x1="15" y1="8" x2="15.01" y2="8"></line>
   <rect x="4" y="4" width="16" height="16" rx="3"></rect>
   <path d="M4 15l4 -4a3 5 0 0 1 3 0l5 5"></path>
   <path d="M14 14l1 -1a3 5 0 0 1 3 0l2 2"></path>
</svg>
`;

const templateString = `
<img id="wrapped-image" class="hidden" />
<svg class="hidden" id="wrapped-svg" xmlns="http://www.w3.org/2000/svg">
</svg>
<style>
:host {
    box-sizing: border-box;
    display: block;
    position: absolute;
    user-select: none;
}

.hidden {
    display: none;
}
img {
    width: 100%;
    height: auto;
    display: block;
}

.currently-wrapped {
    width: 100%;
    height: 100%;
}
</style>
`;

class ImageView extends PartView {
    constructor(){
        super();

        // Setup template and shadow dom
        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(this.template.content.cloneNode(true));

        // Bind component methods
        this.updateImageData = this.updateImageData.bind(this);
        this.updateSvgImage = this.updateSvgImage.bind(this);
        this.updateBinaryImage = this.updateBinaryImage.bind(this);
        this.setDefaultImage = this.setDefaultImage.bind(this);
        this.onClick = this.onClick.bind(this);
        this.loadImageFromSource = this.loadImageFromSource.bind(this);
        this.initCustomHaloButton = this.initCustomHaloButton.bind(this);
        this.updateImageLink = this.updateImageLink.bind(this);
        this.updateSizingForViewport = this.updateSizingForViewport.bind(this);
    }

    afterModelSet(){

        // If the imageData property has a valid value,
        // we want to update the actual image but also
        // clear any `src` property value (without notifying,
        // to prevent infinite recursion)
        this.onPropChange('imageData', (imageData) => {
            if(imageData && imageData !== ""){
                this.model.partProperties.setPropertyNamed(
                    this.model,
                    'src',
                    "",
                    false // do not notify
                );
                this.updateImageData(imageData);
            }
        });

        // If the src property changes, there are a couple
        // of possibilities:
        // 1. There is a valid URL. We reload the image and
        // update all associated properties;
        // 2. There is an invalid URL, but there is imageData
        // present. We do nothing.
        // 3. There is an invalid URL and no imageData present.
        // we load the default placeholder image.
        this.onPropChange('src', (src) => {
            let urlIsValid = (src && src !== "");
            let existingImageData = this.model.partProperties.getPropertyNamed(
                this.model,
                'imageData'
            );
            if(urlIsValid){
                this.loadImageFromSource(src);
            } else {
                this.setDefaultImage();
            }
        });
        
        // Make sure we have imageData. If not, try
        // to load from a src.
        let currentSrc = this.model.partProperties.getPropertyNamed(
            this.model,
            "src"
        );
        let currentImageData = this.model.partProperties.getPropertyNamed(
                this.model,
                "imageData"
            );
        if(currentSrc && currentSrc !== ""){
            this.loadImageFromSource(currentSrc);
        } else if(currentImageData){
            this.updateImageData(currentImageData);
        } else {
            this.setDefaultImage();
        }
        // if(!currentImageData){
        //     if(currentSrc){
        //         let msg = {
        //             type: 'command',
        //             commandName: 'loadImageFrom',
        //             args: [ currentSrc ]
        //         };
        //         this.model.sendMessage(msg, this.model);
        //     } else {
        //         this.setDefaultImage();
        //     }
        // } else {
        //     this.updateImageData(currentImageData);
        // }
    }

    afterConnected(){
        if(!this.haloButton){
            this.initCustomHaloButton();
        }
    }

    afterDisconnected(){
    }

    setDefaultImage(){
        this.model.partProperties.setPropertyNamed(this.model, "imageData", pictureIcon, false);
        this.model.partProperties.setPropertyNamed(this.model, "mimeType", "image/svg");
        this.updateImageData(pictureIcon);
    }

    updateImageData(imageData){
        if(this.model.isSvg){
            this.updateSvgImage(imageData);
        } else {
            this.updateBinaryImage(imageData);
        }
    }

    updateBinaryImage(imageData){
        // In this case, the imageData is
        // a base64 encoded data url describing
        // the bits of the image.
        let imgEl = this._shadowRoot.getElementById('wrapped-image');
        let svgEl = this._shadowRoot.getElementById('wrapped-svg');
        svgEl.classList.add('hidden');
        svgEl.classList.remove('currently-wrapped');
        imgEl.classList.add('currently-wrapped');
        imgEl.src = imageData;
        imgEl.onload = () => {
            //this.updateSizingForViewport();
        };
        this.preserveAspectOnResize = true;
        imgEl.classList.remove('hidden');
    }

    updateSvgImage(imageData){
        let imgEl = this._shadowRoot.getElementById('wrapped-image');
        let currentSvgEl = this._shadowRoot.getElementById('wrapped-svg');
        let parser = new DOMParser();
        let xmlDocument = parser.parseFromString(imageData, 'application/xml');
        let newSvgEl = xmlDocument.documentElement;

        // Ensure that the SVG has some width and height attributes
        // set so we have initial dimensions to display. If not present,
        // pull from viewbox.
        if(!newSvgEl.hasAttribute('width') || !newSvgEl.hasAttribute('height')){
            let viewBox = newSvgEl.getAttribute('viewBox');
            if(viewBox){
                viewBox = viewBox.split(" ");
                let viewBoxWidth = parseInt(viewBox[2]);
                let viewBoxHeight = parseInt(viewBox[3]);
                newSvgEl.setAttribute('height', viewBoxHeight);
                newSvgEl.setAttribute('width', viewBoxWidth);
            }
        } 
        newSvgEl.id = 'wrapped-svg';
        newSvgEl.classList.add('currently-wrapped');
        imgEl.classList.add('hidden');
        imgEl.classList.remove('currently-wrapped');
        currentSvgEl.remove();
        this._shadowRoot.appendChild(newSvgEl);
        this.updateSizingForViewport();
        this.preserveAspectOnResize = false;
    }

    updateImageLink(event){
        // Tells the model to update its
        // src link for the image
        let currentSrc = this.model.partProperties.getPropertyNamed(
            this.model,
            'src'
        );
        let result = window.prompt("Edit URL for image:", currentSrc);
        if(result && result !== currentSrc){
            this.model.partProperties.setPropertyNamed(
                this.model,
                'src',
                result
            );
        } else if(result == ""){
            this.model.partProperties.setPropertyNamed(
                this.model,
                'src',
                result
            );
        }
    }

    updateSizingForViewport(){
        // Ensure that this component does not display larger
        // than the current remaining subrectangle of its origin
        // and the corner of the viewport
        let padding = 40;
        // First, we need to find the absolute top corner
        // locations for the element
        let el = this._shadowRoot.querySelector('.currently-wrapped');
        let top = 0;
        let left = 0;
        while(el){
            top += el.offsetTop;
            left += el.offsetLeft;
            el = el.offsetParent;
        }

        let rect = this.getBoundingClientRect();
        let heightLimit = document.documentElement.clientHeight - padding;
        if((rect.height + top) > heightLimit){
            let ratio = (heightLimit - top) / rect.height;
            // this.style.height = `${rect.height * ratio}px`;
            // this.style.width = `${rect.width * ratio}px`;
            this.model.partProperties.setPropertyNamed(
                this.model,
                'width',
                (rect.width * ratio)
            );
            this.model.partProperties.setPropertyNamed(
                this.model,
                'height',
                (rect.height * ratio)
            );
        }
    }

    loadImageFromSource(sourceUrl){
        fetch(sourceUrl)
            .then(response => {
                let contentType = response.headers.get('content-type');
                if(!contentType.startsWith('image')){
                    throw new Error(`Invalid image mimeType: ${contentType}`);
                }
                this.model.partProperties.setPropertyNamed(
                    this.model,
                    "mimeType",
                    contentType
                );

                if(contentType.startsWith("image/svg")){
                    return response.text().then(text => {
                        // Set the content of the appropriate area
                        // to be SVG inline markup
                        this.updateSvgImage(text);
                    });
                } else {
                    return response.blob().then(blob => {
                        let reader = new FileReader();
                        reader.onloadend = () => {
                            // Set the binary image data
                            this.updateBinaryImage(reader.result);
                        };
                        reader.readAsDataURL(blob);
                    });
                }
            })
            .catch(err => {
                console.error(err);
                this.model.partProperties.setPropertyNamed(
                    this.model,
                    'src',
                    ""
                );
                this.model.partProperties.setPropertyNamed(
                    this.model,
                    'imageData',
                    null
                );
            });
    }

    openHalo(){
        // Override default. Here we add a custom button
        // when showing.
        let foundHalo = this.shadowRoot.querySelector('st-halo');
        if(!foundHalo){
            foundHalo = document.createElement('st-halo');
            this.shadowRoot.appendChild(foundHalo);
        }
        foundHalo.append(this.haloButton);
    }

    onHaloResize(movementX, movementY){
        // Override default behavior.
        // We resize the wrapped svg or img instead
        // and have the outer component simply react to
        // the change.
        // If the part is rotated this will throw off the bounding rectangle
        // browser calcualtion. So the hack here is to rotate the part to 0
        // (if necessary) do the calculations and then rotate it back
        let angle = this.model.partProperties.getPropertyNamed(this.model, "rotate");
        if(angle){
            this.model.partProperties.setPropertyNamed(this.model, "rotate", 0);
        }
        let wrappedImage = this._shadowRoot.querySelector('.currently-wrapped');
        let rect = wrappedImage.getBoundingClientRect();
        let newWidth, newHeight;
        if(this.preserveAspectOnResize){
            let maxWidth = rect.width + movementX;
            let maxHeight = rect.height + movementY;
            let ratio = Math.min(maxWidth / rect.width, maxHeight / rect.height);
            newHeight = rect.height * ratio;
            newWidth = rect.width * ratio;
        } else {
            newWidth = rect.width + movementX;
            newHeight = rect.height + movementY;
        }

        if(newWidth && newHeight){
            this.model.partProperties.setPropertyNamed(
                this.model,
                'width',
                newWidth
            );
            this.model.partProperties.setPropertyNamed(
                this.model,
                'height',
                newHeight
            );
        }
        // reset the rotate angle to the original (if necessary)
        if(angle){
            this.model.partProperties.setPropertyNamed(this.model, "rotate", angle);
        }
    }

    initCustomHaloButton(){
        this.haloButton = document.createElement('div');
        this.haloButton.id = 'halo-image-link';
        this.haloButton.classList.add('halo-button');
        this.haloButton.innerHTML = linkIcon;
        this.haloButton.style.marginTop = "6px";
        this.haloButton.setAttribute('slot', 'right-column');
        this.haloButton.setAttribute('title', 'Edit link for image file');
        this.haloButton.addEventListener('click', this.updateImageLink);
    }

    addContextMenuItems(contextMenu){
        contextMenu.addSpacer();
        contextMenu.addListItem(
            'Edit Image URL',
            this.updateImageLink
        );
    }
};

export {
    ImageView,
    ImageView as default
};


<|start_filename|>js/objects/views/ButtonView.js<|end_filename|>
/**
 * ButtonView
 * ---------------------------------------------------
 * I am a webcomponent representing a Button.
 */
import PartView from './PartView.js';

const templateString = `
                <style>
                 .st-button-label {
                     user-select: none;
                     pointer-events: none;
                     text-overflow: ellipsis;
                     overflow: hidden;
                     max-width: 95%;
                     white-space: nowrap;
                 }
                </style>
                <span class="st-button-label">
                    <slot></slot><!-- Text of the Name -->
                </span>
`;

class ButtonView extends PartView {
    constructor(){
        super();

        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(this.template.content.cloneNode(true));

        // Bound methods
        this.setupPropHandlers = this.setupPropHandlers.bind(this);

        // Setup prop change handlers
        this.setupPropHandlers();
    }

    setupPropHandlers(){
        this.onPropChange('name', (value, partId) => {
            this.innerText = value;
        });
    }

    afterConnected(){
    }

    afterDisconnected(){
    }

    afterModelSet(){
        let buttonName = this.model.partProperties.getPropertyNamed(this, "name");
        if(buttonName){
            this.innerText = buttonName;
        };
    }

    // override the base class implementation
    onClick(event){
        if(event.button == 0){
            if(event.shiftKey){
                this.onHaloActivationClick(event);
            } else if(!this.hasOpenHalo){
                // Send the click command message to self
                this.model.sendMessage({
                    type: 'command',
                    commandName: 'click',
                    args: [],
                    shouldIgnore: true // Should ignore if System DNU
                }, this.model);
            }
        }
    }

    // Overwriting the base class open/close editor methods
    openEditor(){
        window.System.openEditorForPart(this.model.id);
    }

    closeEditor(){
        window.System.closeEditorForPart(this.model.id);
    }
};

export {
    ButtonView,
    ButtonView as default
};


<|start_filename|>js/ohm/tests/test-factor-expression-grammar.js<|end_filename|>
/**
 * Test of Expression and Factor in 
 * grammar parsing.
 */

ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
}
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
}
const matchAndsemanticMatchTest = (str, semanticType) => {
    matchTest(str);
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
}
const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
}
String.prototype.unCapitalize = function() {
    return this.charAt(0).toLowerCase() + this.slice(1);
}


describe('Arithmetic', () => {
    describe('Basic addition', () => {
        it('Can parse a basic literal addition as Expression', () => {
            let str = "2 + 2";
            semanticMatchTest(str, "Expression");
            semanticMatchTest(str, "Expression_addExpr");
        });
        it('Can parse a parens addition as a Factor and Expression', () => {
            let str = "(2 + 2)";
            semanticMatchTest(str, "Factor");
            semanticMatchTest(str, "Expression");
        });
        it('Can parse a basic literal subtraction as Expression', () => {
            let str = "2 - 2";
            semanticMatchTest(str, "Expression");
            semanticMatchTest(str, "Expression_minusExpr");
        });
        it('Can parse a parens subtraction as a Factor and Expression', () => {
            let str = "(2 - 2)";
            semanticMatchTest(str, "Factor");
            semanticMatchTest(str, "Expression");
        });
        it('Can parse a basic literal division as Expression', () => {
            let str = "2 / 2";
            semanticMatchTest(str, "Expression");
            semanticMatchTest(str, "Expression_divideExpr");
        });
        it('Can parse a parens division as a Factor and Expression', () => {
            let str = "(2 / 2)";
            semanticMatchTest(str, "Factor");
            semanticMatchTest(str, "Expression");
        });
        it('Can parse a basic literal modulo division as Expression', () => {
            let str = "2 % 2";
            semanticMatchTest(str, "Expression");
            semanticMatchTest(str, "Expression_moduloDivideExpr");
        });
        it('Can parse a parens modulo division as a Factor and Expression', () => {
            let str = "(2 % 2)";
            semanticMatchTest(str, "Factor");
            semanticMatchTest(str, "Expression");
        });
    });
});

describe('Variable Names', () => {
    it('Can parse a variable name as a Factor', () => {
        let str = 'myCustomVar';
        semanticMatchTest(str, "Factor");
    });
    it('Can parse a variable name as an Expression', () => {
        let str = 'myCustomVar';
        semanticMatchTest(str, "Expression");
    });
});

describe('String Literals', () => {
    it('Can parse a string literal into a Factor', () => {
        let str = '"This is my ... / string literal"';
        semanticMatchTest(str, "Factor");
    });
    it('Can parse a string literal into an Expression', () => {
        let str = '"This is my ... / string literal"';
        semanticMatchTest(str, "Expression");
    });
    it('Can parse a string literal that looks like other expression', () => {
        let str = `"5 + 5"`;
        semanticMatchTest(str, 'stringLiteral');
    });
});

describe('Integer Literals', () => {
    it('Can parse an integer literal into a Factor', () => {
        let str = "22";
        semanticMatchTest(str, "Factor");
    });
    it('Can parse a negative integer literal into a Factor', () => {
        let str = "-111";
        semanticMatchTest(str, "Factor");
    });
    it('Can parse an integer literal into an Expression', () => {
        let str = "22";
        semanticMatchTest(str, "Expression");
    });
    it('Can parse a negative integer literal into an Expression', () => {
        let str = "-22";
        semanticMatchTest(str, "Expression");
    });
});

describe("Float Literals", () => {
    it('Can parse a float literal into a Factor', () => {
        let str = "01.2201";
        semanticMatchTest(str, "Factor");
    });
    it('Can parse a negative float literal into a Factor', () => {
        let str = "-01.2201";
        semanticMatchTest(str, "Factor");
    });
    it('Can parse a float literal into an Expression', () => {
        let str = "00.0001";
        semanticMatchTest(str, "Expression");
    });
    it('Can parse a negative float literal into an Expression', () => {
        let str = "-00.001";
        semanticMatchTest(str, "Expression");
    });
});


describe("Expression Lists (Command Args)", () => {
    it('Can parse a basic two-expression list', () => {
        let str = "1 + 2, 3 + 4";
        semanticMatchTest(str, "CommandArgumentList");
    });
    it('Can parse a parenthetical two-expression list', () => {
        let str = "(1 + 2), (3 + 1.04)";
        semanticMatchTest(str, "CommandArgumentList");
    });
    it('Can parse a float literal and string literal two-expression list', () => {
        let str = `-0.4, "hello there!"`;
        semanticMatchTest(str, "CommandArgumentList");
    });
});

describe("Factors", () => {
    it("Not factor via 'not' (basic)", () => {
        let str = `not 3`;
        semanticMatchTest(str, "Factor_notFactor");
    });
    it("Not factor via 'not' (with expression)", () => {
        let str = `not the "text" of first button`;
        semanticMatchTest(str, "Factor_notFactor");
    });
});


<|start_filename|>js/objects/views/drawing/ColorPickerTool.js<|end_filename|>
/**
 * ColorPickerTool
 * I provide a color choorser capability
 * for the shadow canvas of my parent element.
 * Brushes on my parent Drawing canvas will use
 * whatever color I have currently selected.
 * I am explicitly designed for use with
 * DrawingView*/
import {ColorWheelWidget} from './ColorWheelWidget.js';
const colorPickerSVG = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-color-swatch" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M19 3h-4a2 2 0 0 0 -2 2v12a4 4 0 0 0 8 0v-12a2 2 0 0 0 -2 -2" />
  <path d="M13 7.35l-2 -2a2 2 0 0 0 -2.828 0l-2.828 2.828a2 2 0 0 0 0 2.828l9 9" />
  <path d="M7.3 13h-2.3a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h12" />
  <line x1="17" y1="17" x2="17" y2="17.01" />
</svg>
`;

const colorPickerTemplateString = `
<style>
    :host {
        display: block;
        position: relative;
        margin-bottom: 6px;
    }

    #tool-button {
        --active-color: black;
        --inactive-color: rgb(170, 170, 170);
        --hover-color: rgb(140, 140, 140);
        display: block;
        position: relative;
        border-width: 1px;
        border-style: solid;
        border-color: var(--inactive-color);
        color: var(--inactive-color);
        width: 24px;
        height: 24px;
    }

    :host([active="true"]) > #tool-button {
        border-color: var(--active-color);
        color: var(--active-color);
    }
    color-wheel {
        display: none;
        position: absolute;
    }

    :host([active="true"]) > color-wheel {
        display: flex;
    }
</style>
<div id="tool-button">
${colorPickerSVG}
</div>
<color-wheel></color-wheel>
`;


class ColorPickerTool extends HTMLElement {
    constructor(){
        super();

        // Set up shadow dom. This tool will
        // display itself as a button that can be
        // toggled within its parent DrawingView.
        this.template = document.createElement('template');
        this.template.innerHTML = colorPickerTemplateString;
        this.shadow = this.attachShadow({mode: 'open'});
        this.shadow.append(
            this.template.content.cloneNode(true)
        );

        // Default drawing context is null.
        // This will be set if and when this tool
        // is connected to a parent element
        // that has a context
        this.ctx = null;

        // Bind component methods
        this.start = this.start.bind(this);
        this.end = this.end.bind(this);
        this.onMove = this.onMove.bind(this);
        this.toggleActive = this.toggleActive.bind(this);
        this.onColorSelected = this.onColorSelected.bind(this);
        this.onTransparencyChanged = this.onTransparencyChanged.bind(this);
        this.setContextFromAttributes = this.setContextFromAttributes.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.setAttribute('role', 'tool');
            this.setAttribute('active', false);
            if(!this.hasAttribute('current-color')){
                this.setAttribute('current-color', 'rgba(0, 0, 0, 0)');
            }
            if(this.parentElement.drawingContext){
                this.ctx = this.parentElement.drawingContext;

                // If I am the only tool in my parent,
                // set myself to active
                let siblingTools = this.parentElement.querySelectorAll('[role="tool"]');
                if(siblingTools.length == 1){
                    this.setAttribute('active', true);
                }
            }

            // Attach event listeners
            this.button = this.shadowRoot.getElementById('tool-button');
            this.button.addEventListener('click', this.toggleActive);
            this.colorWheel = this.shadowRoot.querySelector('color-wheel');
            this.colorWheel.addEventListener('color-selected', this.onColorSelected);
            this.colorWheel.addEventListener('transparency-changed', this.onTransparencyChanged);
        }
    }

    disconnectedCallback(){
        this.ctx = null;
        this.button.removeEventListener('click', this.toggleActive);
        this.colorWheel.removeEventListener('color-selected', this.onColorSelected);
        this.colorWheel.removeEventListener('transparency-changed', this.onTransparencyChanged);
    }

    start(x, y){
        // Does nothing in this tool
    }

    end(x, y){
        // Does nothing in this tool
    }

    onMove(x, y){
        // Does nothing in this tool
    }

    setContextFromAttributes(){
        // Does nothing in this tool
    }

    onColorSelected(event){
        let colorInfo = event.detail;
        let colorStr = `rgba(${colorInfo.r}, ${colorInfo.g}, ${colorInfo.b}, ${colorInfo.alpha})`;
        this.ctx.strokeStyle = colorStr;
        this.ctx.fillStyle = colorStr;
    }

    onTransparencyChanged(event){
        this.parentElement.model.sendMessage({
            type: "command",
            commandName: "setProperty",
            args: ["transparency", event.detail]
        }, this.parentElement.model);
    }

    toggleActive(event){
        let isActive = this.getAttribute('active');
        if(isActive == "true"){
            this.setAttribute('active', 'false');
        } else {
            // First, find any other tools in my parent
            // element and deactivate them.
            Array.from(this.parentElement.querySelectorAll('[role="tool"]'))
                .filter(el => {
                    return el.getAttribute('active') == 'true';
                })
                .forEach(el => {
                    el.setAttribute('active', 'false');
                });

            // Set this tool to be active
            this.setAttribute('active', 'true');
        }
    }
};



window.customElements.define('color-picker-tool', ColorPickerTool);

export {
    ColorPickerTool,
    ColorPickerTool as default
};


<|start_filename|>js/objects/ExecutionStack.js<|end_filename|>
/**
 * ExecutionStack
 * ---------------------------------
 * I am an object that manages a stack of
 * ActivationContext objects.
 * I am designed to be used by System as the
 * global execution stack.
 *
 */

class ExecutionStack {
    constructor(){
        this._stack = [];
        this._globals = {};

        // Bound methods
        this.pop = this.pop.bind(this);
        this.push = this.push.bind(this);
        this.setGlobal = this.setGlobal.bind(this);
        this.getGlobal = this.getGlobal.bind(this);
    }

    pop(){
        if(!this._stack.length){
            return null;
        }
        return this._stack.pop();
    }

    push(anActivation){
        this._stack.push(anActivation);
    }

    setGlobal(varName, value){
        this._globals[varName] = value;
    }

    getGlobal(varName){
        return this._globals[varName];
    }

    get current(){
        if(!this._stack.length){
            return null;
        }
        return this._stack[this._stack.length - 1];
    }

    get previous(){
        if(!this._stack.length >= 2){
            return null;
        }
        return this._stack[this._stack.length - 2];
    }
};

class ActivationContext {
    constructor(messageName, part, incomingMessage, handlerFunction){
        this.part = part;
        this.messageName = messageName;
        this.message = incomingMessage;
        this.handlerFunction = handlerFunction;
        this._locals = {};

        // Bound methods
        this.get = this.get.bind(this);
        this.getLocal = this.getLocal.bind(this);
        this.setLocal = this.setLocal.bind(this);
    }

    get(varName){
        let localValue = this.getLocal(varName);
        if(localValue !== undefined){
            return localValue;
        };
        // otherwise try to return a global
        // variable
        return window.System.executionStack.getGlobal(varName);
    }

    getLocal(varName){
        return this._locals[varName];
    }

    setLocal(varName, value){
        this._locals[varName] = value;
    }
}

export {
    ExecutionStack,
    ActivationContext
};


<|start_filename|>js/objects/views/editors/EditorMessenger.js<|end_filename|>
import interpreterSemantics from '../../../ohm/interpreter-semantics.js';

// PREAMBLE
const templateString = `
<style>
    :host {
        display: flex;
        flex-direction: column;
        width: 100%;
        height: 100%;
        font-family: 'Helvetica', sans-serif;
        font-size: 0.8rem;
    }
   
    textarea {
        resize: none;
        flex: 0.25;
    }
</style>
<h3>Send this <span></span> a Message:</h3>
<textarea placeholder="Type your Simpletalk message here..."></textarea>
<button>Send</button>
`;

class EditorMessenger extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Bound methods
        this.render = this.render.bind(this);
        this.onMessageFieldInput = this.onMessageFieldInput.bind(this);
        this.sendMessageFromText = this.sendMessageFromText.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.sendButton = this._shadowRoot.querySelector('button');
            this.sendButton.addEventListener('click', this.sendMessageFromText);

            this.messageField = this._shadowRoot.querySelector('textarea');
            this.messageField.addEventListener('input', this.onMessageFieldInput);
        }
    }

    disconnectedCallback(){
        this.messageField.removeEventListener('input', this.onMessageFieldInput);
        this.sendButton.removeEventListener('click', this.sendMessageFromText);
    }

    render(aModel){
        this.model = aModel;

        let partTypeLabel = this._shadowRoot.querySelector('h3 > span');
        partTypeLabel.textContent = this.model.type;
    }

    onMessageFieldInput(event){

    }

    sendMessageFromText(){
        let text = this.messageField.value + '\n';
        let parsed = window.System.grammar.match(text, 'StatementList');
        if(parsed.succeeded()){
            // it's possible that no script has been compiled on this part
            // and hence no semantics exist
            if(!this.model_semantics){
                this.model._semantics = window.System.grammar.createSemantics();
                this.model._semantics.addOperation(
                    'interpret',
                    interpreterSemantics(this.model, window.System)
                );
            }
            this.model._semantics(parsed).interpret();
        }
    }
};

export {
    EditorMessenger,
    EditorMessenger as default
};


<|start_filename|>js/objects/views/navigator/WrappedView.js<|end_filename|>
/**
 * WrappedView Component
 * ---------------------------------------
 * I am a plain Webcomponent whose purpose is to
 * wrap a visual copy of an actual SimpleTalk View
 * element and display it in a scaled down format.
 * I make a cloned copy of the underlying view and
 * attach it to the same model as the original.
 **/
import PartView from '../PartView.js';

const templateString = `
<style>
    #number-display {
        opacity: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        position: absolute;
        top: 0px;
        left: 0px;
        width: 100%;
        height: 100%;
        transition: opacity 0.2s ease-in;
        font-size: 2.2rem;
    }

    #number-display > span {
        transform: translateY(-10px);
        transition: transform 0.2s linear;
        pointer-events: none;
    }

    :host(:not(.current)) > #number-display {
        opacity: 0.8;
        background-color: rgba(200, 200, 200, 0.5);
        transition: opacity 0.2s ease-out;
        z-index: 1000;
    }

    :host(:not(.current)) > #number-display > span {
        transform: translateY(0px);
        transition: transform 0.2s linear;
    }
</style>
<div id="number-display">
    <span></span>
</div>
<slot name="wrapped-view"></slot>
`;


class WrappedView extends PartView {
    constructor(){
        super();
        this.wantsHalo = false;

        // Set up template and shadowDom
        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            this.template.content.cloneNode(true)
        );

        // Bind methods
        this.onChildSlotted = this.onChildSlotted.bind(this);
        this.updateScaling = this.updateScaling.bind(this);
        this.handleNumberChange = this.handleNumberChange.bind(this);
        this.addWrappedView = this.addWrappedView.bind(this);
        this._recursivelyUpdateLensViews = this._recursivelyUpdateLensViews.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            // Bind a listener for the slot change.
            // This will be triggered whenever any
            // underlying element is slotted, so we
            // know to recompute the appropriate sizing
            // and styling
            let slotEl = this._shadowRoot.querySelector('slot');
            slotEl.addEventListener('slotchange', this.onChildSlotted);

            // we don't want the context menu to open in the nav since
            // it doens't make sense atm and will error
            this.removeEventListener('contextmenu', this.onContextMenuClick);
        }
    }

    disconnectedCallback(){
        let slotEl = this._shadowRoot.querySelector('slot');
        slotEl.removeEventListener('slotchange', this.onChildSlotted);
    }

    onChildSlotted(event){
        //this.updateScaling();
        //this.updateNumberDisplay();
    }

    afterModelSet(){
        this.onPropChange('number', this.handleNumberChange);
        this.removeAttribute('part-id');
        this.addWrappedView(this.model);
        this.updateNumberDisplay();
    }

    updateScaling(){
        let firstChild = this.children[0];
        // We need to find the element corresponding to the
        // actual view for the lens-ed part, in order to get
        // its current dimensions.
        let partId = firstChild.getAttribute('lens-part-id');
        let refElement = document.querySelector(`st-world`);
        let wrapBox = this.getBoundingClientRect();
        let innerBox = document.querySelector(`st-world`).getBoundingClientRect();
        let scalingX = (wrapBox.width / innerBox.width);
        let refElementBox = refElement.getBoundingClientRect();
        firstChild.style.width = `${refElementBox.width}px`;
        firstChild.style.height = `${refElementBox.height}px`;
        firstChild.style.transform = `scale(${scalingX})`;
        firstChild.style.transformOrigin = "0px 0px";
    }

    updateNumberDisplay(){
        let firstChild = this.children[0];
        let model = firstChild.model;
        // we only want to look at subparts of the same type (stack or card)
        let subparts = model._owner.subparts.filter((part) => {
            return model.type == part.type;
        });
        let numDisplay = this._shadowRoot.querySelector('#number-display > span');
        numDisplay.innerText = subparts.indexOf(model) + 1;
    }

    handleNumberChange(){
        // Update number display of all wrapped views in the row
        Array.from(this.parentNode.querySelectorAll(`wrapped-view`)).forEach(wrapper => {
            wrapper.updateNumberDisplay();
        });
    }

    addWrappedView(aPartModel){
        // First, clear out any existing
        // child elements
        this.innerHTML = "";

        // Create a lensed copy of the given
        // view and update key attributes on it
        let originalView = document.querySelector(`[part-id="${aPartModel.id}"]`);
        let lensedView = originalView.cloneNode(true);
        lensedView.setAttribute('lens-part-id', aPartModel.id);
        lensedView.setAttribute('slot', 'wrapped-view');
        lensedView.style.pointerEvents = "none";
        lensedView.wantsHalo = false;

        // Inline the initial scaling style properties.
        // We begin with an extremely small amount which will
        // be adjusted later during updateScaling();
        lensedView.style.transform = `scale(${0.001})`;
        lensedView.style.transformOrigin = "0px 0px";
        
        // Recursively create lens views of all subpart children
        // and append them in the correct places
        lensedView.isLensed = true;
        lensedView.setModel(aPartModel);
        lensedView.removeAttribute('part-id');
        if(lensedView.handleCurrentChange){
            lensedView.handleCurrentChange();
        }
        this._recursivelyUpdateLensViews(lensedView, aPartModel.id);

        // Insert the root lensed view into the wrapper
        this.setAttribute('wrapped-id', aPartModel.id);
        this.appendChild(lensedView);
        this.updateScaling();
    }

    _recursivelyUpdateLensViews(lensedView, anId){
        let subViews = Array.from(lensedView.children).filter(child => {
            return child.isPartView;
        });
        subViews.forEach(subView => {
            subView.isLensed = true;
            subView.wantsHalo = false;
            let subId = subView.getAttribute('part-id');
            subView.setAttribute('lens-part-id', subId);
            let model = window.System.partsById[subId];
            subView.setModel(model);
            subView.removeAttribute('part-id');
            this._recursivelyUpdateLensViews(subView, subId);
        });
    }

    /** PartView Overrides **/
    styleCSS(){
        // Do nothing
    }

    styleTextCSS(){
        // Do nothing
    }

    layoutChanged(){
        // Do nothing
    }
};

export {
    WrappedView,
    WrappedView as default
};


<|start_filename|>js/objects/System.js<|end_filename|>
/**
 * System Object
 * -------------------------------------
 * I am an object representing the "top" of the
 * sytem. I am the point of communication between
 * Models and Views.
 */
import Card from './parts/Card.js';
import Stack from './parts/Stack.js';
import Button from './parts/Button.js';
import Field from './parts/Field.js';
import WorldStack from './parts/WorldStack.js';
import Window from './parts/Window.js';
import Drawing from './parts/Drawing.js';
import Audio from './parts/Audio.js';
import Browser from './parts/Browser.js';
import Resource from './parts/Resource.js';
import Image from './parts/Image.js';
import Area from './parts/Area.js';

import WorldView from './views/WorldView.js';
import StackView from './views/StackView.js';
import ButtonView from './views/ButtonView.js';

import CardView from './views/CardView.js';
import WindowView from './views/WindowView';
import FieldView from './views/FieldView.js';
import DrawingView from './views/drawing/DrawingView.js';
import ImageView from './views/ImageView.js';
import AreaView from './views/AreaView.js';
import AudioView from './views/AudioView.js';
import BrowserView from './views/BrowserView.js';
import ResourceView from './views/ResourceView.js';


import Halo from './views/Halo.js';
import STNavigator from './views/navigator/Navigator.js';
import Editor from './views/editors/Editor.js';

import ohm from 'ohm-js';
import interpreterSemantics from '../ohm/interpreter-semantics.js';
import {ExecutionStack, ActivationContext} from './ExecutionStack.js';

import idMaker from './utils/id.js';
import STClipboard from './utils/clipboard.js';

import {BasicProperty} from './properties/PartProperties.js';

import handInterface from './utils/handInterface.js';
import merriamSimScore from './utils/merriamInterface.js';

import {STDeserializer, STSerializer} from './utils/serialization.js';

import plugins from '../../plugins/plugins.js';

const DOMparser = new DOMParser();

import createHighlighter from './utils/AltSyntaxHighlighter.js';


const System = {
    name: "System",
    id: -1,
    isLoaded: false,
    partsById: {},
    _commandHandlers: {},
    _functionHandlers: {},
    _deserializer: null,

    // A dictionary mapping available ST resource (such as plugin) names
    availableResources: {},

    // A dictionary mapping part types like
    // 'button' to their classes (Button)
    availableParts: {},

    // A dictionary mapping part types like
    // 'button' to their view classes (ButtonView)
    availableViews: {},

    // A registry that keeps all system messages from
    // beginnign of time; TODO in the future we might want
    // to note keep all this in memory
    // each log consists of:
    // [aMessage, (sourceName, sourceId), (targetName, targetId)]
    messageLog: [],

    // Will be called when a page loads.
    // Checks for any view elements in the
    // page and attempts to find serialized
    // model state for each of them. If present,
    // deserializes the model and attaches it
    // to the view.
    initialLoad: function(){
        // load the available resources
        // these might be needed down the line
        this.loadResources();

        // If we have a serialization script tag
        // containing JSON of serialized information,
        // attempt to load from it
        let serializationEl = document.getElementById('serialization');
        if(serializationEl && serializationEl.text != ""){
            this.deserialize()
                .then(() => {
                    this.sendInitialOpenMessages();
                    System.navigator.setModel(
                        System.partsById['world']
                    );

                    // By default, we render the World in the
                    // Comprehensive Editor
                    //this.editor.render(this.world);
                });
        } else {
            this.loadFromEmpty();
 
            // By default, we render the World in the
            // Comprehensive Editor
            this.editor.render(this.world);
        }

        // Attach a new clipboard instance
        this.clipboard = new STClipboard(this);

        // By this point we should have a WorldView with
        // a model attached.
        this.isLoaded = true;
    },

    loadResources: function() {
        Object.keys(plugins).forEach((k) => {
            this.availableResources[k] = plugins[k];
        });
    },

    loadFromEmpty: function(){
        let worldModel = new this.availableParts['world']();
        this.partsById[worldModel.id] = worldModel;
        let worldView = document.createElement(
            this.tagNameForViewNamed('world')
        );
        worldView.setModel(worldModel);
        document.body.appendChild(worldView);

        // Create initial stack model
        let initStack = this.newModel('stack', worldModel.id);

        // Create initial card model for that stack
        let initCard = this.newModel('card', initStack.id);

        // Update current stack and card ids 
        worldModel.partProperties.setPropertyNamed(
            worldModel,
            'current',
            initStack.id
        );
        initStack.partProperties.setPropertyNamed(
            initStack,
            'current',
            initCard.id
        );
        // Update serialization
        this.serialize();

        this.sendInitialOpenMessages();
        System.navigator.setModel(
            System.partsById['world']
        );
    },

    sendInitialOpenMessages: function(){
        // Send the openWorld message to the WorldStack
        let world = this.partsById['world'];
        world.sendMessage({
            type: 'command',
            commandName: 'openWorld',
            args: [],
            shouldIgnore: true
        }, world);
        world.sendMessage({
            type: 'command',
            commandName: 'openStack',
            args: [],
            shouldIgnore: true
        }, world.currentStack);
        world.currentStack.sendMessage({
            type: 'command',
            commandName: 'openCard',
            args: [],
            shouldIgnore: true
        }, world.currentStack.currentCard);
    },

    sendMessage: function(aMessage, source, target){
        if(!target || target == undefined){
            throw new Error('Messages must be sent with target receivers specified!');
        }

        // keep track of all sources which pass this message
        if (!("senders" in aMessage)) {
            aMessage["senders"] = [];
        }
        aMessage.senders.push({
            name: source.name,
            id: source.id,
        });

        return target.receiveMessage(aMessage);
    },

    receiveMessage: function(aMessage){
        switch(aMessage.type){
            case 'newView':
                return this.newView(
                    aMessage.viewType,
                    aMessage.modelId
                );
            case 'compile':
                return this.compile(aMessage);
            case 'command':
                return this.receiveCommand(aMessage);
            default:
                return this.doesNotUnderstand(aMessage);
        }
    },

    doesNotUnderstand: function(aMessage){
        // If the message has the shouldIgnore property
        // set to true, it means we should just swallow
        // this message if we don't understand it. This is
        // useful for messages like mouse events on buttons
        // which are not captured by default and would otherwise
        // end up arriving to this System object via the
        // message delegation chain.
        if(aMessage.shouldIgnore){
            return;
        }
        let originalSender = this.partsById[aMessage.senders[0].id];
        let msg = {
            type: "error",
            name: "MessageNotUnderstood",
            message: aMessage
        };
        originalSender.sendMessage(msg, originalSender);
    },

    compile: function(aMessage){
        let targetObject = this.partsById[aMessage.targetId];
        if(!targetObject || targetObject == undefined){
            throw new Error(`System could not compile target object ${aMessage.targetId}`);
        }


        // Attempt to parse the incoming SimpleTalk script string.
        // If there are grammatical errors, report them and bail.
        // Otherwise, create a new semantics on the targetPart, add
        // the required semantic operations, and interpret the top
        // level of the script, which will create the JS handler functions
        let parsedScript = languageGrammar.match(aMessage.codeString);
        if(parsedScript.failed()){
            // consider using the parse data from trace
            // example: let tracedScript = languageGrammar.trace(aMessage.codeString);
            // let tree = tracedScript.toString();
            let msg = {
                type: "error",
                name: "GrammarMatchError",
                parsedScript: parsedScript,
                partId: aMessage.targetId
            };
            targetObject.sendMessage(msg, targetObject);
        } else {
            // First, clear out any currently compiled handlers
            // since the incoming script might get rid of them
            targetObject._commandHandlers = {};

            // Create a semantics object whose partContext
            // attribute is set to be the target object.
            targetObject._semantics = languageGrammar.createSemantics();
            targetObject._semantics.addOperation(
                'interpret',
                interpreterSemantics(targetObject, this)
            );
            targetObject._semantics(parsedScript).interpret();
        }


        // Be sure to then update the
        // serialization for the target
        // part, thus adding the script to
        // its serialization
        if(aMessage.serialize){
            this.serialize();
        }
    },

    receiveCommand: function(aMessage){
        let handler = this._commandHandlers[aMessage.commandName];
        if(handler){
            let boundHandler = handler.bind(this);
            let activation = new ActivationContext(
                aMessage.commandName,
                this,
                aMessage,
                boundHandler
            );
            this.executionStack.push(activation);
            var result = boundHandler(aMessage.senders, ...aMessage.args);
            this.executionStack.pop();
            return result;
        } else {
            return this.doesNotUnderstand(aMessage);
        }
    },

    newModel: function(kind, ownerId, name, buildView=true){
        // Lookup the instance of the model that
        // matches the owner's id
        let ownerPart = this.partsById[ownerId];
        if(!ownerPart || ownerPart == undefined){
            throw new Error(`System could not locate owner part with id ${ownerId}`);
        }

        // Find the class constructor for the kind of
        // part requested as a new model. If not known,
        // throw an error
        let modelClass = this.availableParts[kind];
        if(!modelClass){
            throw new Error(`Cannot create unknown part type: ${kind}`);
        }
        let model = new modelClass(ownerPart);
        if(name){
            model.partProperties.setPropertyNamed(model, 'name', name);
        }
        this.partsById[model.id] = model;

        // Any created part might initialize its
        // own subparts. We need to let the System know
        // about those too
        model.subparts.forEach(subpart => {
            this.partsById[subpart.id] = subpart;
        });

        // If there is a valid owner part for
        // the newly created part model,
        // add the new model to the owner's
        // subparts list
        if(ownerPart){
            ownerPart.addPart(model);
        }

        // Add the System as a property subscriber to
        // the new model. This will send a message to
        // this System object whenever any of this model's
        // properties have changed
        model.addPropertySubscriber(this);

        if(buildView){
            // See if there is already a view for the model.
            // If not, create and attach it.
            let viewForModel = this.findViewById(model.id);
            if(!viewForModel){
                this.newView(model.type, model.id);
            }
        }

        // If the owner part is either a world or a stack
        // and has only one stack or card child, respectively, set
        // that child to be the current
        if(ownerPart.type == "world" || ownerPart.type == "stack"){
            let currentId = ownerPart.partProperties.getPropertyNamed(ownerPart, "current");
            if(!currentId){
                ownerPart.partProperties.setPropertyNamed(ownerPart, "current", model.id);
            }
        }

        // Finally send the model a message to announce that it is new
        // to the world, but don't 
        this.sendMessage({
            type: 'command',
            commandName: 'newModel',
            args: [],
            shouldNotDelegate: true,
            shouldIgnore: true
        }, this, model);

        return model;
    },

    newProperty(senders, propName, objectId){
        let target;
        let originalSender = senders[0].id;

        if(objectId){
            // Otherwise, if there is an objectId, we are
            // setting the property of a specific part by
            // id
            target = this.partsById[objectId];
        } else {
            // Otherwise we are setting the property on the part
            // that originally sent the message
            target = this.partsById[originalSender];
        }

        if(!target){
            throw new Error(`Could not find newProperty target!`);
        }

        if(target.partProperties.findPropertyNamed(propName)){
            // TODO this should be a ST error
            throw new Error(`Part ${target.id} already has property "${propName}"`);
        }
        // we only add basic property and the default value is null
        let customProp = target.partProperties.findPropertyNamed("custom-properties");
        let newProp = new BasicProperty(propName, null);
        customProp.add(newProp);
    },

    deleteProperty(senders, propName, objectId){
        let target;
        let originalSender = senders[0].id;

        if(objectId){
            // Otherwise, if there is an objectId, we are
            // setting the property of a specific part by
            // id
            target = this.partsById[objectId];
        } else {
            // Otherwise we are setting the property on the part
            // that originally sent the message
            target = this.partsById[originalSender];
        }

        if(!target){
            throw new Error(`Could not find deleteProperty target!`);
        }

        // Note: this will only delete custom properties which is what we want
        let prop = target.partProperties.findPropertyNamed(propName);

        let customProp = target.partProperties.findPropertyNamed("custom-properties");
        customProp.delete(prop);
    },

    setProperty(senders, propName, value, objectId){
        let target;
        let originalSender = senders[0].id;

        if(objectId){
            // Otherwise, if there is an objectId, we are
            // setting the property of a specific part by
            // id
            target = this.partsById[objectId];
        } else {
            // Otherwise we are setting the property on the part
            // that originally sent the message
            target = this.partsById[originalSender];
        }

        if(!target){
            throw new Error(`Could not find setProperty target!`);
        }

        target.partProperties.setPropertyNamed(
            target,
            propName,
            value,
        );
    },

    // Remove the model with the given ID from
    // the System's registry, as well as from the subparts
    // array of any owners
    deleteModel: function(modelId){
        let foundModel = this.partsById[modelId];
        if(!foundModel){
            return false;
        }
        // When removing a card or a stack be sure it is not the only one
        // and if it is the current card or stack we should go to the next one
        // before removing it
        if(foundModel.type == "card" || foundModel.type == "stack"){
            let sameTypeSubparts = foundModel._owner.subparts.filter((p) => {return p.type == foundModel.type;});
            if(sameTypeSubparts.length == 1){
                // TODO this should be a ST error
                throw new Error(`Cannot remove the only ${foundModel.type}`);
            } else if(modelId == this.getCurrentStackModel().id  || modelId == this.getCurrentCardModel().id){
                // TODO this should be a ST error
                throw new Error(`Cannot remove the current ${foundModel.type}`);
            }
        }

        // Make sure to stop all stepping
        // on the Part, otherwise stepping
        // intervals will error infinitely
        foundModel.stopStepping();

        // make sure the editor is closed
        foundModel.closeEditorCmdHandler();

        let ownerModel = foundModel._owner;
        if(ownerModel){
            ownerModel.removePart(foundModel);
        }

        delete this.partsById[modelId];
        this.removeViews(modelId);

        // Serialize the state
        this.serialize();
        return true;
    },

    // Remove all views with the corresponding
    // model id from the DOM
    removeViews: function(modelId){
        let views = Array.from(this.findViewsById(modelId));
        views.forEach(view => {
            let parentEl = view.parentElement;
            view.parentElement.removeChild(view);
            // Dispatch a CustomEvent on the parentElement
            // indicating that this part has been removed, and
            // any view utilities that care can be notified.
            let event = new CustomEvent('st-view-removed', {
                detail: {
                    partType: view.model.type,
                    partId: modelId,
                    ownerId: null
                } 
            });
            parentEl.dispatchEvent(event);
        });
        let lenses = this.findLensViewsById(modelId);
        lenses.forEach(lensView => {
            lensView.parentElement.removeChild(lensView);
        });
    },

    newView: function(partName, modelId, parentId){
        let model = this.partsById[modelId];
        if(!model || model == undefined){
            throw new Error(`System does not know part ${partName}[${modelId}]`);
        }
        if(!partName){
            partName = model.type;
        }

        // Find the parent model id. This will
        // help us find the parent view element for
        // appending the new element.
        if (!parentId){
            parentId = model._owner.id;
        }
        let parentElement = this.findViewById(parentId);
        if(!parentElement){
            throw new Error(`Could not find parent element for ${partName}[${modelId}] (model owner id: ${model._owner.id})`);
        }

        // Create the new view instance,
        // append to parent, and set the model
        let newView = document.createElement(
            this.tagNameForViewNamed(partName)
        );
        newView.setModel(model);
        this.sendMessage({
            type: "viewChanged",
            changeName: "subpart-new",
            args: [newView]
        }, model._owner, parentElement);

        // Dispatch a CustomEvent on the parentElement
        // indicating that this part has been created, and
        // any view utilities that care can be notified.
        let event = new CustomEvent('st-view-added', {
            detail: {
                partType: model.type,
                partId: model.id,
                ownerId: model._owner.id || null
            } 
        });
        parentElement.dispatchEvent(event);

        // See if there are lens views and update
        // those as well
        let lensViews = this.findLensViewsById(parentId);
        lensViews.forEach(lensView => {
            let newLensView = document.createElement(
                this.tagNameForViewNamed(partName)
            );
            newLensView.setModel(model);
            newLensView.removeAttribute('part-id');
            newLensView.setAttribute('lens-part-id', modelId);
            newLensView.setAttribute('role', 'lens');
            lensView.appendChild(newLensView);
        });

        // TODO do we want to allow the possibiliy of a view on an
        // element but no subpart of that view on the element?

        // For all subparts of this model, call
        // the newView method recursively
        model.subparts.forEach(subpart => {
            this.newView(subpart.type, subpart.id);
        });

        return newView;
    },

    registerPart: function(name, cls){
        this.availableParts[name] = cls;
    },

    registerView: function(name, cls){
        this.availableViews[name] = cls;
    },

    tagNameForViewNamed: function(name){
        return `st-${name}`;
    },

    // Find the first matching view element
    // with the given id
    findViewById: function(id){
        return document.querySelector(`[part-id="${id}"]`);
    },

    findLensViewsById: function(id){
        return Array.from(document.querySelectorAll(`[lens-part-id="${id}"]`));
    },

    // Find all matching view elements with
    // the given part id
    findViewsById: function(id){
        return document.querySelectorAll(`[part-id="${id}"]`);
    },

    // return the model corresponding to the current stack
    getCurrentStackModel: function(){
        let world = this.getWorldStackModel();
        return world.currentStack;
    },

    // return the model corresponding to the current card
    getCurrentCardModel: function(){
        let currentStack = this.getCurrentStackModel();
        return currentStack.currentCard;
    },

    // return the model corresponding to the world stack
    getWorldStackModel: function(){
        return this.partsById['world'];
    },

    // return the model corresponding script editor st-field
    // Note: we use the window.model.target to locate the corresponding window
    // but return its st-field subpart
    findScriptEditorByTargetId: function(id){
        let scriptEditorField;
        let windows = document.querySelectorAll("st-window");
        windows.forEach((w) => {
            let target = w.model.target;
            if(target && target.id === id){
                scriptEditorField = w.querySelector("st-field");
            }
        });
        return scriptEditorField;
    },

    serialize: function(){
        let world = this.partsById['world'];
        if(!world){
            throw new Error(`No world found!`);
        }
        let serializer = new STSerializer(this);
        let serialString = serializer.serialize(this.partsById['world'], true);

        // If there is not a script tag in the
        // body for the serialization, create it
        let serializationScriptEl = document.getElementById('serialization');
        if(!serializationScriptEl){
            serializationScriptEl = document.createElement('script');
            serializationScriptEl.id = 'serialization';
            serializationScriptEl.type = 'application/json';
            document.body.append(serializationScriptEl);
        }
        serializationScriptEl.textContent = serialString;
    },

    deserialize: function(){
        let serializationEl = document.getElementById('serialization');
        if(!serializationEl){
            throw new Error(`No serialization found for this page`);
        }
        this._deserializer = new STDeserializer(this);
        return this._deserializer.deserialize(serializationEl.textContent);
    },

    // Return a *complete* HTML
    // representation of the current application
    // that can later be saved to a file
    getFullHTMLString: function(){
        let clonedDocument = document.cloneNode(true);
        let world = clonedDocument.querySelector('st-world');
        let nav = clonedDocument.querySelector('st-navigator');
        if(world){
            world.remove();
        }
        if(nav){
            nav.remove();
        }

        return clonedDocument.documentElement.outerHTML;
    },


    /** Navigation of Current World **/
    goToNextStack: function(){
        let world = this.partsById['world'];
        return world.goToNextStack();
    },

    goToPrevStack: function(){
        let world = this.partsById['world'];
        return world.goToPrevStack();
    },

    goToStackById: function(stackId){
        let world = this.partsById['world'];
        return world.goToStackById(stackId);
    },

    /** Navigation of Current Stack **/
    goToNextCard: function(){
        let currentStack = this.getCurrentStackModel(); 
        return currentStack.goToNextCard();
    },

    goToPrevCard: function(){
        let currentStack = this.getCurrentStackModel();
        return currentStack.goToPrevCard();
    },

    goToCardById: function(cardId){
        let currentStack = this.getCurrentStackModel();
        return currentStack.goToCardById(cardId);
    },

    openEditorForPart: function(partId){
        this.editor.render(
            this.partsById[partId]
        );
        this.editor.open();
    },

    closeEditorForPart: function(partId){
        this.editor.close();
    }
};

/** Add Default System Command Handlers **/
//System._commandHandlers['deleteModel'] = System.deleteModel;
System._commandHandlers['deleteModel'] = function(senders, ...rest){
    System.deleteModel(...rest);
};
//System._commandHandlers['newModel'] = System.newModel;
System._commandHandlers['newModel'] = function(senders, ...rest){
    System.newModel(...rest);
    this.serialize();
};
//System._commandHandlers['newView'] = System.newView;
System._commandHandlers['newView'] = function(senders, ...rest){
    System.newView(...rest);
};
System._commandHandlers['newProperty'] = System.newProperty;
System._commandHandlers['setProperty'] = System.setProperty;
System._commandHandlers['deleteProperty'] = System.deleteProperty;

System._commandHandlers['ask'] = function(senders, question){
    // Use the native JS prompt function to ask the question
    // and return its value.
    // By returning here, we expect the implicit variable
    // "it" to be set inside any calling script
    return prompt(question);
};

System._commandHandlers['putInto'] = function(senders, value, variableName, global){
    if(global){
        System.executionStack.setGlobal(variableName, value);
        return;
    }
    // Because we push all handlers onto the execution stack,
    // the putInto handler is currently at the top of the stack.
    // In order to modify the caller's variables, we need to
    // find the context that is one previous on the stack
    if(System.executionStack.previous){
        System.executionStack.previous.setLocal(variableName, value);
    } else {
        throw new Error(`ExecutionStack Error: #putInto on top of empty stack!`);
    }
};

System._commandHandlers['answer'] = function(senders, value){
    alert(value.toString());
};

System._commandHandlers['go to direction'] = function(senders, directive, objectName){
    switch(objectName) {
        case 'card':
            switch(directive){
                case 'next':
                    this.goToNextCard();
                    break;

                case 'previous':
                    this.goToPrevCard();
                    break;
            }
            break;

        case 'stack':
            switch(directive){
                case 'next':
                    this.goToNextStack();
                    break;

                case 'previous':
                    this.goToPrevStack();
                    break;
            }
            break;

        default:
            alert(`"go to" not implemented for ${objectName}`);

    }
};

System._commandHandlers['go to'] = function(senders, id){
    let model = this.partsById[id];
    if(!model){
        alert(`"go to" target not found`);
    }
    switch(model.type) {
    case 'card':
        this.goToCardById(id);
        break;

    case 'stack':
        this.goToStackById(id);
        break;

    default:
        alert(`"go to" not implemented for ${model.type}`);

    }
};

System._commandHandlers['go to website'] = function(senders, url){
    window.location.href = url;
};

//Import a world, i.e. its stacks from another source
System._commandHandlers['importWorld'] = function(sender, sourceUrl){
    if(!sourceUrl){
        sourceUrl = window.prompt("Choose World location");
    }
    fetch(sourceUrl)
        .then(response => {
            let contentType = response.headers.get('content-type');
            if(!contentType.startsWith('text/html')){
                throw new Error(`Invalid content type: ${contentType}`);
            }
            return response.blob().then(blob => {
                let reader = new FileReader();
                reader.readAsText(blob);
                reader.onloadend = () => {
                    let parsedDocument = DOMparser.parseFromString(reader.result, "text/html");
                    // there is no .getElementById() for a node HTML parsed document!
                    let serializationEl = parsedDocument.querySelector('#serialization');
                    if(!serializationEl){
                        console.log(`No serialization found for ${sourceUrl}`);
                        alert(`World "${sourceUrl}" not found`);
                        return;
                    }
                    this._deserializer = new STDeserializer(this);
                    this._deserializer.targetId = 'world'; // We will insert the stacks into the world
                    return this._deserializer.importFromSerialization(
                        serializationEl.textContent,
                        (part) => {
                            // Return only Stacks that are direct subparts
                            // of the world.
                            let isStack = part.type == 'stack';
                            let isWorldSubpart = part._owner && part._owner.type == 'world';
                            return isStack && isWorldSubpart;
                        }
                    );
                };
            });
        })
        .then(() => {
            // Manually set the _src.
            // This ensures that we don't infinitely
            // call the load operation
            this._src = sourceUrl;
            /*
            // Stop and restart hand interface if it's running.
            if (handInterface.handDetectionRunning) {
                handInterface.stop();
                handInterface.start();
            }
            */
        })
        .catch(err => {
            alert("Could not load world");
            console.error(err);
        });
};

System._commandHandlers['openScriptEditor'] = function(senders, targetId){
    let target = this.partsById[targetId];
    let targetName = target.partProperties.getPropertyNamed(target, "name");
    if(targetName){
        targetName = `"${targetName}"`;
    }
    let name = `Script For ${target.name} ${targetName}`;

    // If there is already a dinwo opened with this name, then
    // we return without creating anything new.
    let found = Object.values(System.partsById).filter(part => {
        let partName;
        if(part.type == 'window'){
            partName = part.partProperties.getPropertyNamed(
                part,
                'title'
            );
        }
        return (part.type == 'window' && name == partName);
    });
    if(found.length){
        return;
    }

    let currentCard = this.getCurrentCardModel();
    let window = this.newModel('window', currentCard.id);
    let area = this.newModel('area', window.id);
    let scriptField = this.newModel('field', area.id);
    let saveButton = this.newModel('button', area.id);


    // setup the window and stack properties
    window.setTarget(target);
    window.partProperties.setPropertyNamed(window, "title", name);
    window.partProperties.setPropertyNamed(window, "height", 200);
    window.partProperties.setPropertyNamed(window, "width", 500);
    window.partProperties.setPropertyNamed(window, 'top', 100);
    window.partProperties.setPropertyNamed(window, 'left', 100);

    area.partProperties.setPropertyNamed(area, "layout", "list");
    area.partProperties.setPropertyNamed(area, "list-direction", "column");
    area.partProperties.setPropertyNamed(area, "width", "fill");
    area.partProperties.setPropertyNamed(area, "height", "fill");

    // script field
    let targetScript = target.partProperties.getPropertyNamed(target, "script"); 
    scriptField.partProperties.setPropertyNamed(scriptField, "text", targetScript);
    scriptField.partProperties.setPropertyNamed(scriptField, "height", "fill");
    scriptField.partProperties.setPropertyNamed(scriptField, "width", "fill");

    // Setup syntax highlight
    scriptField.sendMessage({
        type: "command",
        commandName: "highlightSyntax",
        args: []
    }, scriptField);


    // setup up the save button properties
    saveButton.partProperties.setPropertyNamed(saveButton, "name", "Save Script");
    saveButton.partProperties.setPropertyNamed(saveButton, "width", "fill");
    saveButton.partProperties.setPropertyNamed(saveButton, "text-size", 20);
    saveButton.partProperties.setPropertyNamed(saveButton, "target", `part id ${target.id}`);

    saveButton.partProperties.setPropertyNamed(saveButton, "target", `part id ${target.id}`);
    let saveScript = `on click\n\ttell target to set "script" to the "text" of first field\nend click`; 
    saveButton.partProperties.setPropertyNamed(saveButton, "script", saveScript);
};

System._commandHandlers['SimpleTalk'] = function(senders){
    return System.grammar.source.sourceString;
}

System._commandHandlers['openDebugger'] = function(senders, partId, type, name){
    let target = this.partsById[partId];
    // Create the Field model and attach to current card
    let currentCard = this.getCurrentCardModel();
    let windowModel = this.newModel('window', currentCard.id);
    let areaModel = this.newModel('area', windowModel.id);
    // name the window and setup basic layout
    windowModel.partProperties.setPropertyNamed(
        windowModel,
        'name',
        "debugger"
    );
    windowModel.partProperties.setPropertyNamed(
        windowModel,
        'title',
        `Message Not Understood : ${name}`
    );
    windowModel.partProperties.setPropertyNamed(
        windowModel,
        'width',
        400
    );
    windowModel.partProperties.setPropertyNamed(
        windowModel,
        'height',
        200
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'layout',
        "list"
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'list-direction',
        "column"
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'width',
        "fill"
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'height',
        "fill"
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'allow-scrolling',
        true
    );
    // fill in all the available commands for the given part
    Object.keys(target.commandHandlerRegistry).forEach((key) =>{
        let text = key;
        let isPrivate = target.commandHandlerRegistry[key].private;
        if(isPrivate){
            text += " (private)";
        }
        let fieldModel = this.newModel('field', areaModel.id);
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'text',
            text
        );
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'editable',
            false
        );
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'width',
            "fill"
        );
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'height',
            20
        );
    });
};

System._commandHandlers['openGrammar'] = function(senders, partId, ruleName){
    // first make sure that there are no grammar windows open already
    let windows = Object.values(window.System.partsById).filter((part) => {return part.name == "Window";});
    let grammarWindows = windows.filter((window) => {
        return window.partProperties.getPropertyNamed(window, "name") == "Simpletalk Grammar";
    });
    if(grammarWindows.length){
        return;
    }
    let target = this.partsById[partId];
    // Create the Field model and attach to current card
    let currentCard = this.getCurrentCardModel();
    let windowModel = this.newModel('window', currentCard.id);
    let areaModel = this.newModel('area', windowModel.id);
    // name the window and setup basic layout
    windowModel.partProperties.setPropertyNamed(
        windowModel,
        'name',
        "Simpletalk Grammar"
    );
    windowModel.partProperties.setPropertyNamed(
        windowModel,
        'title',
        "Simpletalk Grammar"
    );
    windowModel.partProperties.setPropertyNamed(
        windowModel,
        'width',
        400
    );
    windowModel.partProperties.setPropertyNamed(
        windowModel,
        'height',
        200
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'layout',
        "list"
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'list-direction',
        "column"
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'width',
        "fill"
    );
    areaModel.partProperties.setPropertyNamed(
        areaModel,
        'height',
        "fill"
    );
    // add the grammar text
    let text = System.grammar.source.sourceString;
    let fieldModel = this.newModel('field', areaModel.id);
    fieldModel.partProperties.setPropertyNamed(
        fieldModel,
        'text',
        text
    );
    fieldModel.partProperties.setPropertyNamed(
        fieldModel,
        'editable',
        false
    );
    fieldModel.partProperties.setPropertyNamed(
        fieldModel,
        'width',
        "fill"
    );
    fieldModel.partProperties.setPropertyNamed(
        fieldModel,
        'height',
        "fill"
    );
};

System._commandHandlers['saveHTML'] = function(senders){
    // Stop hand recognition if it's running.
    let handRecognitionOriginallyRunning = handInterface.handDetectionRunning;
    if (handRecognitionOriginallyRunning) {
        handInterface.stop();
    }
    this.serialize();

    let stamp = Date.now().toString();
    let serializedPage = this.getFullHTMLString();
    let typeInfo = "data:text/plain;charset=utf-8";
    let url = `${typeInfo},${encodeURIComponent(serializedPage)}`;

    let anchor = document.createElement('a');
    anchor.style.display = "none";
    document.body.append(anchor);
    anchor.href = url;
    anchor.download = `SimpleTalkSnapshot_${stamp}.html`;
    anchor.click();
    window.URL.revokeObjectURL(url);
    anchor.parentElement.removeChild(anchor);
    // Start hand recognition if it was running.
    if (handRecognitionOriginallyRunning) {
        handInterface.start();
    }
};

System._commandHandlers['tell'] = (senders, targetId, deferredMessage) => {
    let targetPart = System.partsById[targetId];
    if(!targetPart){
        throw new Error(`Attempted to tell part id ${targetId}: no such part!`);
    }
    targetPart.sendMessage(deferredMessage, targetPart);
};

System._commandHandlers['toggleHandDetection'] = () => {
    if (handInterface.handDetectionModel === null) {
        handInterface.start();
    } else {
        handInterface.stop();
    }
};

System._commandHandlers['merriam'] = (senders, docId) => {
    const sender = System.partsById[senders[0].id];
    merriamSimScore(sender, docId);
};

System._commandHandlers['globalInterrupt'] = () => {
    // cycle through all the parts and set the "stepping" property to false
    Object.values(System.partsById).forEach((part) => {
        if(part.isStepping){
            part.partProperties.setPropertyNamed(part, "stepping", false);
        }
    });
};


/** Register the initial set of parts in the system **/
System.registerPart('card', Card);
System.registerPart('stack', Stack);
System.registerPart('field', Field);
System.registerPart('button', Button);
System.registerPart('world', WorldStack);
System.registerPart('window', Window);
System.registerPart('field', Field);
System.registerPart('drawing', Drawing);
System.registerPart('image', Image);
System.registerPart('area', Area);
System.registerPart('audio', Audio);
System.registerPart('browser', Browser);
System.registerPart('resource', Resource);

/** Register the initial set of views in the system **/
System.registerView('button', ButtonView);
System.registerView('stack', StackView);
System.registerView('world', WorldView);
System.registerView('card', CardView);
System.registerView('window', WindowView);
System.registerView('field', FieldView);
System.registerView('drawing', DrawingView);
System.registerView('image', ImageView);
System.registerView('area', AreaView);
System.registerView('audio', AudioView);
System.registerView('browser', BrowserView);
System.registerView('resource', ResourceView);


// Convenience method for adding all of the
// available custom elements to the window object's
// customElements registry
System.registerCustomElements = function(){
    Object.keys(System.availableViews).forEach(partName => {
        let viewClass = System.availableViews[partName];
        let elementName = System.tagNameForViewNamed(partName);
        window.customElements.define(elementName, viewClass);
    });
};

// iniitalize the compiler and add it to the system
// Instantiate the grammar.
let languageGrammar;
if (window.grammar){
    // for testing it is sometimes convenient to load the grammar and add to window
    // see ./tests/preload.js for example
    languageGrammar = ohm.grammar(window.grammar);
} else {
    languageGrammar = ohm.grammarFromScriptElement();
}

System.grammar = languageGrammar;

// Set the exection stack on the
// System
System.executionStack = new ExecutionStack();

// Add a dynamic getter for the World for convenience
Object.defineProperty(System, 'world', {
    get: function(){
        return this.partsById['world'];
    }
});

document.addEventListener('DOMContentLoaded', () => {
    // Add the System object to window so
    // that it is global on the page. We do this
    // for both debugging and testing.
    window.System = System;
    // Add the possible views as webcomponents
    // in the custom element registry
    System.registerCustomElements();

    // Add any other non-part view CustomElements,
    // like the halo
    window.customElements.define('st-halo', Halo);
    window.customElements.define('st-navigator', STNavigator);
    window.customElements.define('st-editor', Editor);

    // Add nav
    System.navigator = document.createElement('st-navigator');
    document.body.appendChild(System.navigator);

    // Add comprehensive editor pane
    // if one is not already present in the markup
    let existingEditors = Array.from(document.querySelectorAll('st-editor'));
    existingEditors.forEach(editorEl => {
        editorEl.remove();
    });
    System.editor = document.createElement('st-editor');
    document.body.appendChild(System.editor);

    // Perform the initial setup of
    // the system
    System.initialLoad();
});

// add a message listener on window
// these can include message from a browser part
// for now filter those not coming from the origin
window.addEventListener("message", (event) => {
    if (event.origin !== window.origin)
        return;
    console.log(`Message to browser`);
    // TODO: maybe we need to deal with quote escapes directly
    // in the grammar
    let script = event.data.replaceAll("'", '"');
    // only statements are accepted for now
    let parsedScript = languageGrammar.match(script, "Statement");
    if(parsedScript.failed()){
        // for now just log that it failed
        console.log("failed to parse script for browser");
    } else {
        let semantics = languageGrammar.createSemantics();
        let world = System.partsById["world"];
        semantics.addOperation(
            'interpret',
            interpreterSemantics(world, System)
        );
        let msg = semantics(parsedScript).interpret();
        System.sendMessage(msg, world, System);
    }
});

// global interrupt
document.addEventListener('keydown', (event) => {
    if(event.ctrlKey && event.key == 'c'){
        System.sendMessage({
            type: "command",
            commandName: "globalInterrupt",
            args: []
        }, System, System);
    }
});


export {
    System,
    System as default
};


<|start_filename|>js/objects/tests/test-object-speficier-with-preload.js<|end_filename|>
import chai from 'chai';
import ohm from 'ohm-js';
const assert = chai.assert;
const expect = chai.expect;

import interpreterSemantics from '../../ohm/interpreter-semantics.js';

let testLanguageGrammar = ohm.grammar(window.grammar);

let fields = [];
let buttons = [];
let cards = [];
let currentCardModel = null;
let area = null;

// Helper function that adds cards, buttons, and
// fields to the current stack.
// We add 3 buttons and fields each to
// three cards total (including the current one)
// in an alternating pattern
function setupCardsAndParts(stack){
    for(let i = 0; i < 3; i++){
        let currentCard = window.System.getCurrentCardModel();
        window.System.newModel(
            'button',
            currentCard.id,
            `Button ${1}`
        );
        window.System.newModel(
            'field',
            currentCard.id,
            `Field ${1}`
        );
        window.System.newModel(
            'button',
            currentCard.id,
            `Button ${2}`
        );
        window.System.newModel(
            'field',
            currentCard.id,
            `Field ${2}`
        );
        window.System.newModel(
            'button',
            currentCard.id,
            `Button ${3}`
        );
        window.System.newModel(
            'field',
            currentCard.id,
            `Field ${3}`
        );
        let areaModel = window.System.newModel(
            'area',
            currentCard.id,
            `Area ${1}`
        );
        window.System.newModel(
            'button',
            areaModel.id,
            `Area Button ${1}`
        );

        if(i < 2){
            window.System.newModel(
                'card',
                stack.id,
                'stack'
            );
            window.System.goToNextCard();
        }
    }
}

describe("ObjectSpecifier Tests", () => {
    describe("Model Setup", () => {
        it("Can find the current card model", () => {
            let currentCardView = document.querySelector('.current-stack > .current-card');
            currentCardModel = currentCardView.model;
            assert.exists(currentCardModel);
        });
        it("Can add all the cards and test parts without error", () => {
            let stack = window.System.getCurrentStackModel();
            let setup = function(){
                return setupCardsAndParts(stack);
            };
            expect(setup).to.not.throw();
            // add one more stack and do the same
            let stack2 = window.System.newModel("stack", "world", "Stack 2");
            setup = function(){
                return setupCardsAndParts(stack2);
            };
            expect(setup).to.not.throw();
        });
        it("Can load the language grammar", () => {
            assert.exists(testLanguageGrammar);
        });
    });
    describe("Simple specifiers with current card in context", () => {
        let semantics;
        let partContext;

        before(() => {
            partContext = window.System.getCurrentCardModel();
            semantics = testLanguageGrammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(partContext, window.System)
            ); 
        });
        it("Can match the button 1 of current card", () => {
            let str = `button 1 of current card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(part => {
                return part.type == 'button';
            })[0];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match second field of current card", () => {
            let str = `second field of current card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(part => {
                return part.type == 'field';
            })[1];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match part 3 of the current card", () => {
            let str = `part 3 of current card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts[2];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match the fourth part of the current card", () => {
            let str = `fourth part of current card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts[3];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match a field by name", () => {
            let str = `field "Field 2" of current card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(subpart => {
                return subpart.type == 'field';
            })[1];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
    });
    describe("Simple specifiers with a 'this' card in context", () => {
        let semantics;
        let partContext;

        before(() => {
            // We set the 'this card' part context to be the 2nd card in the stack, which
            // is different from the current card (which is the 3rd card at this point).
            // Remember that 'current card' and 'this card' can be different, if the script
            // calling it is in a part that is not within the currently displayed card
            partContext = window.System.getCurrentStackModel().subparts.filter(subpart => {
                return subpart.type == 'card';
            })[1];
            semantics = testLanguageGrammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(partContext, window.System)
            ); 
        });

        it("Can match the button 1 of this card", () => {
            let str = `button 1 of this card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(part => {
                return part.type == 'button';
            })[0];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match second field of this card", () => {
            let str = `second field of this card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(part => {
                return part.type == 'field';
            })[1];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match part 3 of the this card", () => {
            let str = `part 3 of this card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts[2];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match the fourth part of the this card", () => {
            let str = `fourth part of this card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts[3];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match a field by name", () => {
            let str = `field "Field 2" of this card`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(subpart => {
                return subpart.type == 'field';
            })[1];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
    });

    describe("Simple specifiers with implicit card context (equiv. to 'this')", () => {
        let semantics;
        let partContext;

        before(() => {
            // we set the 
            semantics = testLanguageGrammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(partContext, window.System)
            ); 
        });

        it.skip("Can match the button 1 (implicit this card)", () => {
            let str = `button 1`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(part => {
                return part.type == 'button';
            })[0];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it.skip("Can match second field (implicit this card)", () => {
            let str = `second field`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(part => {
                return part.type == 'field';
            })[1];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it.skip("Can match part 3 of (implicit this card)", () => {
            let str = `part 3`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts[2];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it.skip("Can match the fourth part (implicit this card)", () => {
            let str = `fourth part`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts[3];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it.skip("Can match a field by name (implicit this card)", () => {
            let str = `field "Field 2"`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(subpart => {
                return subpart.type == 'field';
            })[1];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
    });
    describe("Simple 'target' specifiers", () => {
        let semantics;
        let partContext;

        before(() => {
            // We set the 'this card' part context to be the 2nd card in the stack, which
            // is different from the current card (which is the 3rd card at this point).
            // Remember that 'current card' and 'this card' can be different, if the script
            // calling it is in a part that is not within the currently displayed card
            partContext = window.System.getCurrentStackModel().subparts.filter(subpart => {
                return subpart.type == 'card';
            })[1];
            semantics = testLanguageGrammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(partContext, window.System)
            ); 
        });

        it("Can match the button 1 (implicit this card)", () => {
            let str = `button 1`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(part => {
                return part.type == 'button';
            })[0];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match second field (implicit this card)", () => {
            let str = `second field`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(part => {
                return part.type == 'field';
            })[1];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match part 3 of (implicit this card)", () => {
            let str = `part 3`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts[2];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match the fourth part (implicit this card)", () => {
            let str = `fourth part`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts[3];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can match a field by name (implicit this card)", () => {
            let str = `field "Field 2"`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let expectedPart = partContext.subparts.filter(subpart => {
                return subpart.type == 'field';
            })[1];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
    });

    describe("Complex Specifiers with current stack in context", () => {
        let semantics;
        let partContext;

        before(() => {
            partContext = window.System.getCurrentStackModel();
            semantics = testLanguageGrammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(partContext, window.System)
            ); 
        });

        it("Can get button 1 of card 2 of current stack", () => {
            let str = `button 1 of card 2 of current stack`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let secondCard = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[1];
            let expectedPart = secondCard.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can get third field of second card of current stack", () => {
            let str = `third field of second card of current stack`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let secondCard = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[1];
            let expectedPart = secondCard.subparts.filter(subpart => {
                return subpart.type == 'field';
            })[2];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
    });
    describe("Complex Specifiers with non-current stack", () => {
        let semantics;
        let partContext;

        before(() => {
            partContext = window.System.getCurrentStackModel();
            semantics = testLanguageGrammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(partContext, window.System)
            ); 
        });

        it("Can get current card of current stack", () => {
            let str = `current card of current stack`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let currentStack = window.System.getCurrentStackModel();
            let expectedPart = currentStack.currentCard;
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can get current card of second stack", () => {
            let str = `current card of second stack`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let world = window.System.partsById["world"];
            let secondStack = world.subparts.filter(subpart => {
                return subpart.type == 'stack';
            })[1];
            let expectedPart = secondStack.currentCard;
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
    });
    describe("Complex Specifiers without terminal", () => {
        describe("Context is current card", () => {
            let semantics;
            let partContext;

            before(() => {
                partContext = window.System.getCurrentCardModel();
                semantics = testLanguageGrammar.createSemantics();
                semantics.addOperation(
                    'interpret',
                    interpreterSemantics(partContext, window.System)
                );
            });

            it("Can get button 1 of area 1 ", () => {
                let str = `button 1 of area 1`;
                let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
                assert.isTrue(matchObject.succeeded());
                let firstArea = partContext.subparts.filter(subpart => {
                    return subpart.type == 'area';
                })[0];
                let expectedPart = firstArea.subparts.filter(subpart => {
                    return subpart.type == 'button';
                })[0];
                let expectedValue = expectedPart.id;
                let result = semantics(matchObject).interpret();
                assert.equal(expectedValue, result);
            });
            it("Can get button 1 of area 1 of card 1", () => {
                let str = `button 1 of area 1 of card 1`;
                let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
                assert.isTrue(matchObject.succeeded());
                let firstCard = window.System.getCurrentStackModel().subparts.filter(subpart => {
                    return subpart.type == 'card';
                })[0];
                let firstArea = firstCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                })[0];
                let expectedPart = firstArea.subparts.filter(subpart => {
                    return subpart.type == 'button';
                })[0];
                let expectedValue = expectedPart.id;
                let result = semantics(matchObject).interpret();
                assert.equal(expectedValue, result);
            });
            it("Can get button 1 of area 1 of card 1 of stack 1", () => {
                let str = `button 1 of area 1 of card 1 of first stack`;
                let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
                assert.isTrue(matchObject.succeeded());
                let firstCard = window.System.getCurrentStackModel().subparts.filter(subpart => {
                    return subpart.type == 'card';
                })[0];
                let firstArea = firstCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                })[0];
                let expectedPart = firstArea.subparts.filter(subpart => {
                    return subpart.type == 'button';
                })[0];
                let expectedValue = expectedPart.id;
                let result = semantics(matchObject).interpret();
                assert.equal(expectedValue, result);
            });
        });
        describe("Context is current area of current card", () => {
            let semantics;
            let partContext;

            before(() => {
                let currentCard = window.System.getCurrentCardModel();
                partContext = currentCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                })[0];

                semantics = testLanguageGrammar.createSemantics();
                semantics.addOperation(
                    'interpret',
                    interpreterSemantics(partContext, window.System)
                );
            });

            it("Can get button 1 of area 1 ", () => {
                let str = `button 1`;
                let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
                assert.isTrue(matchObject.succeeded());
                let expectedPart = partContext.subparts.filter(subpart => {
                    return subpart.type == 'button';
                })[0];
                let expectedValue = expectedPart.id;
                let result = semantics(matchObject).interpret();
                assert.equal(expectedValue, result);
            });
            it("Can get button 1 of area 1 of card 1", () => {
                let str = `button 1 of area 1 of card 1`;
                let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
                assert.isTrue(matchObject.succeeded());
                let firstCard = window.System.getCurrentStackModel().subparts.filter(subpart => {
                    return subpart.type == 'card';
                })[0];
                let firstArea = firstCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                })[0];
                let expectedPart = firstArea.subparts.filter(subpart => {
                    return subpart.type == 'button';
                })[0];
                let expectedValue = expectedPart.id;
                let result = semantics(matchObject).interpret();
                assert.equal(expectedValue, result);
            });
            it("Can get button 1 of area 1 of card 1 of stack 1", () => {
                let str = `button 1 of area 1 of card 1 of first stack`;
                let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
                assert.isTrue(matchObject.succeeded());
                let firstCard = window.System.getCurrentStackModel().subparts.filter(subpart => {
                    return subpart.type == 'card';
                })[0];
                let firstArea = firstCard.subparts.filter(subpart => {
                    return subpart.type == 'area';
                })[0];
                let expectedPart = firstArea.subparts.filter(subpart => {
                    return subpart.type == 'button';
                })[0];
                let expectedValue = expectedPart.id;
                let result = semantics(matchObject).interpret();
                assert.equal(expectedValue, result);
            });
        });
    });
    describe("Target specifiers", () => {
        let semantics;
        let partContext;

        before(() => {
            partContext = window.System.getCurrentStackModel();
            semantics = testLanguageGrammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(partContext, window.System)
            );
            // we need to add the semantics to the partContext since
            // we are not actually compiling a script on it
            // however OHM will throw an grammar(System)-semantics(partContext) mismatch error if we
            // simply set the above semantics on the partContext
            partContext._semantics = semantics;
            window.System.grammar = semantics.getGrammar();
        });

        it("Can get button 1 of card 2 of current stack from target of button 1 of card 1", () => {
            // set up the target prop
            let card1 = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[0];
            let button1Card1 = card1.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            button1Card1.partProperties.setPropertyNamed(
                button1Card1,
                'target',
                'button 1 of card 2 of current stack'
            );
            let str = `target of button 1 of card 1 of current stack`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let secondCard = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[1];
            let expectedPart = secondCard.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can get button 1 of area 1 of card 1 of current stack from target of button 1 of card 1", () => {
            // set up the target prop
            let card1 = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[0];
            let button1Card1 = card1.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            button1Card1.partProperties.setPropertyNamed(
                button1Card1,
                'target',
                'button 1 of first area of card 1 of current stack'
            );
            let str = `target of button 1 of card 1 of current stack`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            let area = card1.subparts.filter(subpart => {
                return subpart.type == 'area';
            })[0];
            let expectedPart = area.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            let expectedValue = expectedPart.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can get target of a target", () => {
            // set up the target prop
            let card1 = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[0];
            let button1Card1 = card1.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            button1Card1.partProperties.setPropertyNamed(
                button1Card1,
                'target',
                'button 1 of card 2 of current stack'
            );
            let card2 = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[1];
            let button1Card2 = card2.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            button1Card2.partProperties.setPropertyNamed(
                button1Card2,
                'target',
                'button 1 of card 3 of current stack'
            );
            let str = `target of target of button 1 of card 1 of current stack`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            // check the object ids
            let card3 = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[2];
            let button1Card3 = card3.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            let expectedValue = button1Card3.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
        it("Can get target of a target of a target", () => {
            // set up the target prop
            let card1 = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[0];
            let button1Card1 = card1.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            button1Card1.partProperties.setPropertyNamed(
                button1Card1,
                'target',
                'button 1 of card 2 of current stack'
            );
            let card2 = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[1];
            let button1Card2 = card2.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            button1Card2.partProperties.setPropertyNamed(
                button1Card2,
                'target',
                'button 1 of card 3 of current stack'
            );
            let card3 = partContext.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[2];
            let button1Card3 = card3.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            button1Card3.partProperties.setPropertyNamed(
                button1Card3,
                'target',
                'button 1 of card 1 of current stack' // back to card1 button1
            );
            let str = `target of target of target of button 1 of card 1 of current stack`;
            let matchObject = testLanguageGrammar.match(str, 'ObjectSpecifier');
            assert.isTrue(matchObject.succeeded());
            // check the object ids
            let expectedValue = button1Card1.id;
            let result = semantics(matchObject).interpret();
            assert.equal(expectedValue, result);
        });
    });
});


<|start_filename|>js/objects/tests/test-styling-with-preload.js<|end_filename|>
/**
 *  Style and related property testing 
 * ----------------------------------
 */

import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;
import cssStyler from '../utils/styler.js';

let currentCardModel;
let buttonModel;
let stylerObj;

describe('CSS Styler Util', () => {
    before(() => {
        stylerObj = {};
    });
    describe('Text', () => {
        it('Text font conversion', () => {
            cssStyler(stylerObj, "text-font", "Times");
            assert.equal(stylerObj["fontFamily"], "Times");
        });
        it('Text color conversion', () => {
            cssStyler(stylerObj, "text-color", "rgb(255, 0, 0)");
            assert.equal(stylerObj["color"], "rgba(255, 0, 0, 1)");
            cssStyler(stylerObj, "text-color", "red");
            assert.equal(stylerObj["color"], "rgba(255, 0, 0, 1)");
        });
        it('Text transparency', () => {
            cssStyler(stylerObj, "text-transparency", .5);
            assert.equal(stylerObj["color"], "rgba(255, 0, 0, 0.5)");
            cssStyler(stylerObj, "text-transparency", ".5");
            assert.equal(stylerObj["color"], "rgba(255, 0, 0, .5)");
        });
    });
    describe('Background', () => {
        it('Background color conversion', () => {
            cssStyler(stylerObj, "background-color", "rgb(0, 0, 0)");
            assert.equal(stylerObj["backgroundColor"], "rgba(0, 0, 0, 1)");
            cssStyler(stylerObj, "background-color", "red");
            assert.equal(stylerObj["backgroundColor"], "rgba(255, 0, 0, 1)");
        });
        it('Background transparency', () => {
            cssStyler(stylerObj, "background-transparency", .5);
            assert.equal(stylerObj["backgroundColor"], "rgba(255, 0, 0, 0.5)");
            cssStyler(stylerObj, "background-transparency", ".5");
            assert.equal(stylerObj["backgroundColor"], "rgba(255, 0, 0, .5)");
        });
    });
    describe('Borders', () => {
        it('Border Style (single side)', () => {
            cssStyler(stylerObj, "border-top-style", "solid");
            assert.equal(stylerObj["border-top-style"], "solid");
            assert.isUndefined(stylerObj["border-bottom-style"]);
            assert.isUndefined(stylerObj["border-left-style"]);
            assert.isUndefined(stylerObj["border-right-style"]);
            cssStyler(stylerObj, "border-right-style", "dashed");
            assert.equal(stylerObj["border-right-style"], "dashed");
            cssStyler(stylerObj, "border-left-style", "inset");
            assert.equal(stylerObj["border-left-style"], "inset");
            cssStyler(stylerObj, "border-bottom-style", "outset");
            assert.equal(stylerObj["border-bottom-style"], "outset");
        });
        it('Border Width', () => {
            cssStyler(stylerObj, "border-top-width", 100);
            assert.equal(stylerObj["border-top-width"], "100px");
            assert.isUndefined(stylerObj["border-bottom-width"]);
            assert.isUndefined(stylerObj["border-left-width"]);
            assert.isUndefined(stylerObj["border-right-width"]);
            cssStyler(stylerObj, "border-right-width", "20");
            assert.equal(stylerObj["border-right-width"], "20px");
            cssStyler(stylerObj, "border-left-width", 10);
            assert.equal(stylerObj["border-left-width"], "10px");
            cssStyler(stylerObj, "border-bottom-width", "1");
            assert.equal(stylerObj["border-bottom-width"], "1px");
        });
        it('Border Color', () => {
            cssStyler(stylerObj, "border-top-color", "rgb(255, 255, 255)");
            assert.equal(stylerObj["border-top-color"], "rgba(255, 255, 255, 1)");
        });
        it('Border Color Keyword', () => {
            cssStyler(stylerObj, "border-top-color", "red");
            assert.equal(stylerObj["border-top-color"], "rgba(255, 0, 0, 1)");
        });
        it('Border Transparency', () => {
            cssStyler(stylerObj, "border-top-transparency", "0.5");
            assert.equal(stylerObj["border-top-color"], "rgba(255, 0, 0, 0.5)");
        });
    });
    describe('Corners', () => {
        it('Round corner', () => {
            cssStyler(stylerObj, "corner-top-left-round", "10");
            assert.equal(stylerObj["border-top-left-radius"], "10px");
            assert.isUndefined(stylerObj["border-top-right-radius"]);
            assert.isUndefined(stylerObj["border-bottom-left-radius"]);
            assert.isUndefined(stylerObj["border-bottom-right-radius"]);
            cssStyler(stylerObj, "corner-top-right-round", "10");
            assert.equal(stylerObj["border-top-right-radius"], "10px");
            cssStyler(stylerObj, "corner-bottom-right-round", "10");
            assert.equal(stylerObj["border-bottom-right-radius"], "10px");
            cssStyler(stylerObj, "corner-bottom-left-round", "10");
            assert.equal(stylerObj["border-bottom-left-radius"], "10px");
        });
    });
    describe('Shadows', () => {
        it('Color', () => {
            cssStyler(stylerObj, "shadow-color", "rgb(255, 255, 255)");
            assert.equal(stylerObj["box-shadow"], "0px 0px 0px 0px rgba(255, 255, 255, 1)");
        });
        it('Transparency', () => {
            cssStyler(stylerObj, "shadow-transparency", 0.5);
            assert.equal(stylerObj["box-shadow"], "0px 0px 0px 0px rgba(255, 255, 255, 0.5)");
        });
        it('Left', () => {
            cssStyler(stylerObj, "shadow-left", 10);
            assert.equal(stylerObj["box-shadow"], "10px 0px 0px 0px rgba(255, 255, 255, 0.5)");
        });
        it('Top', () => {
            cssStyler(stylerObj, "shadow-top", 20);
            assert.equal(stylerObj["box-shadow"], "10px 20px 0px 0px rgba(255, 255, 255, 0.5)");
        });
        it('Blur', () => {
            cssStyler(stylerObj, "shadow-blur", 30);
            assert.equal(stylerObj["box-shadow"], "10px 20px 30px 0px rgba(255, 255, 255, 0.5)");
        });
        it('Spread', () => {
            cssStyler(stylerObj, "shadow-spread", 40);
            assert.equal(stylerObj["box-shadow"], "10px 20px 30px 40px rgba(255, 255, 255, 0.5)");
        });
    });
    it('Transparency', () => {
        cssStyler(stylerObj, "transparency", 0.5);
        assert.equal(stylerObj["opacity"], 0.5);
        cssStyler(stylerObj, "transparency", "0.5");
        assert.equal(stylerObj["opacity"], "0.5");
    });
    it('Hide', () => {
        cssStyler(stylerObj, "hide", false);
        assert.equal(stylerObj["display"], null);
        cssStyler(stylerObj, "hide", true);
        assert.equal(stylerObj["display"], "none");
    });
    it('Background Color style conversion', () => {
        cssStyler(stylerObj, "background-color", "red");
        assert.equal(stylerObj["backgroundColor"], "rgba(255, 0, 0, 1)");
    });
    it('Unit px properties (top, left) conversion', () => {
        cssStyler(stylerObj, "top", 2);
        assert.equal(stylerObj["top"], "2px");
        cssStyler(stylerObj, "top", "2");
        assert.equal(stylerObj["top"], "2px");
        cssStyler(stylerObj, "top", "2px");
        assert.equal(stylerObj["top"], "2px");
        cssStyler(stylerObj, "top", null);
        assert.equal(stylerObj["top"], "2px");
    });
    it('Unit rotate(deg) property conversion', () => {
        cssStyler(stylerObj, "rotate", null);
        assert.equal(stylerObj["transform"], undefined);
        cssStyler(stylerObj, "rotate", 20);
        assert.equal(stylerObj["transform"], "rotate(20deg)");
        cssStyler(stylerObj, "rotate", -20);
        assert.equal(stylerObj["transform"], "rotate(-20deg)");
    });
    it('Null or undefined value styles do not update the style object', () => {
        cssStyler(stylerObj, "background-color", null);
        assert.equal(stylerObj["backgroundColor"], "rgba(255, 0, 0, 1)");
        cssStyler(stylerObj, "background-color", undefined);
        assert.equal(stylerObj["backgroundColor"], "rgba(255, 0, 0, 1)");
    });
    it('Styles updated properly', () => {
        cssStyler(stylerObj, "background-color", "black");
        assert.equal(stylerObj["backgroundColor"], "rgba(0, 0, 0, 1)");
        cssStyler(stylerObj, "text-color", "black");
        assert.equal(stylerObj["color"], "rgba(0, 0, 0, 1)");
    });
});

describe('Styling Properties', () => {
    before(() => {
        let currentCardView = document.querySelector('.current-stack > .current-card');
        currentCardModel = currentCardView.model;
        assert.exists(currentCardModel);
        let addButton = function(){
            let msg = {
                type: 'command',
                commandName: 'newModel',
                args: [
                    'button',
                    currentCardModel.id,
                    'card'
                ]
            };
            currentCardModel.sendMessage(msg, currentCardModel);
        };
        expect(addButton).to.not.throw(Error);
        buttonModel = currentCardModel.subparts.filter(subpart => {
            return subpart.type == 'button';
        })[0];
        assert.exists(buttonModel);
    });
    describe('cssStyle', () => {
        it('Initial button "cssStyle" BasicProperty is properly set', () => {
            // Note we just test for some of the core style props as the
            // complete list is likely to change in the future
            let styleProp = buttonModel.partProperties.getPropertyNamed(buttonModel, "cssStyle");
            assert.equal(styleProp['top'], "0px");
            assert.equal(styleProp['left'], "0px");
        });
        it('Initial button DOM element style attribute is properly set', () => {
            // Note we just test for some of the core style props as the
            // complete list is likely to change in the future
            let buttonView = window.System.findViewById(buttonModel.id);
            assert.equal(buttonView.style['top'], "0px");
            assert.equal(buttonView.style['left'], "0px");
        });
        it('Updating StyleProperty directly updates the style property value', () => {
            let styleProp = buttonModel.partProperties.getPropertyNamed(buttonModel, "hide");
            assert.equal(styleProp, false);
            buttonModel.partProperties.setPropertyNamed(buttonModel, "hide", true);
            styleProp = buttonModel.partProperties.getPropertyNamed(buttonModel, "hide");
            assert.equal(styleProp, true);
        });
        it('Updating StyleProperty directly updates the "cssStyle" BasicProperty', () => {
            let styleProp = buttonModel.partProperties.getPropertyNamed(buttonModel, "cssStyle");
            assert.equal(styleProp['display'], 'none');
        });
        it('Updating StyleProperty directly updates the DOM element style attribute', () => {
            let buttonView = window.System.findViewById(buttonModel.id);
            assert.equal(buttonView.style['display'], 'none');
        });
        it('Updating StyleProperty via "set" message updates the "cssStyle" BasicProperty', () => {
            let msg  = {
                type: "command",
                commandName: "setProperty",
                args: ["hide", false]
            };
            buttonModel.sendMessage(msg, buttonModel);
            let styleProp = buttonModel.partProperties.getPropertyNamed(buttonModel, "cssStyle");
            assert.equal(styleProp['display'], null);
        });
        it('Updating StyleProperty via "set" updates the DOM element style attribute', () => {
            let buttonView = window.System.findViewById(buttonModel.id);
            assert.equal(buttonView.style['display'], '');
        });
    });
    describe('cssTextStyle', () => {
        it('Initial button "cssTextStyle" BasicProperty is properly set', () => {
            // Note we just test for some of the core style props as the
            // complete list is likely to change in the future
            let styleProp = buttonModel.partProperties.getPropertyNamed(buttonModel, "cssTextStyle");
            assert.equal(styleProp['textAlign'], 'left');
        });
        it('Initial button DOM element style attribute is properly set', () => {
            // Note we just test for some of the core style props as the
            // complete list is likely to change in the future
            let buttonView = window.System.findViewById(buttonModel.id);
            assert.equal(buttonView.style['textAlign'], 'left');
        });
        it('Updating a text style related StyleProperty', () => {
            // Note we just test for some of the core style props as the
            // complete list is likely to change in the future
            buttonModel.partProperties.setPropertyNamed(buttonModel, "text-align", "center");
            let styleProp = buttonModel.partProperties.getPropertyNamed(buttonModel, "cssTextStyle");
            assert.equal(styleProp['textAlign'], 'center');
        });
        it('Updating "cssTextStyle" property has DOM element style attribute is properly set', () => {
            // Note we just test for some of the core style props as the
            // complete list is likely to change in the future
            let buttonView = window.System.findViewById(buttonModel.id);
            assert.equal(buttonView.style['textAlign'], 'center');
        });
    });
});


<|start_filename|>js/ohm/tests/test-if-then-else-grammar.js<|end_filename|>
/**
 * If Then Else Grammar Tests
 * -------------------------------
 * Tests for basic non-loop control flow
 * grammar.
 * SimpleTalk takes the HyperTalk pattern of having three variants:
 * 1. A single-line `if <condition> then <command>`
 * 2. A multiline:
 *     `if <condition>
 *      then <command>`
 * 3. A multine, with an `else`:
 *     `if <condition>
 *      then <command>
 *      else <command>`
 *
 */
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
};
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};


describe("Basic IfThenInline", () => {
    it("Can handle basic comparison", () => {
        let str = `if 2 == 2 then go to next card`;
        semanticMatchTest(str, "IfThenInline");
    });
    it("Can handle comparison with variable", () => {
        let str = `if myVariable is 34 then put 25 into myVariable`;
        semanticMatchTest(str, "IfThenInline");
    });
    it("Can handle a literal true", () => {
        let str = `if true then go to next card`;
        semanticMatchTest(str, "IfThenInline");
    });
    it("Can handle a literal false", () => {
        let str = `if false then go to next card`;
        semanticMatchTest(str, "IfThenInline");
    });
    it("Can handle a literal true comparison", () => {
        let str = `if myVariable is true then go to next card`;
        semanticMatchTest(str, "IfThenInline");
    });
    it("Can handle a literal false comparison", () => {
        let str = `if myVariable is false then go to next card`;
        semanticMatchTest(str, "IfThenInline");
    });
    it("Can handle implicit true", () => {
        let str = `if myVariable then go to next card`;
        semanticMatchTest(str, "IfThenInline");
    });
    it("Can handle implicit false", () => {
        let str = `if not myVariable then go to next card`;
        semanticMatchTest(str, "IfThenInline");
    });
    it("Is recognized as a Statement", () => {
        let str = "if myVariable then go to next card";
        semanticMatchTest(str, "Statement");
    });
});

describe("Basic IfThenSingleline", () => {
    describe("Without 'else'", () => {
        it("Can handle basic comparison", () => {
            let str = [
                'if 2 == 2',
                'then go to next card'
            ].join('\n');
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle comparison with a variable", () => {
            let str = [
                'if myVariable is 34',
                'then put 35 into myVariable'
            ].join('\n');
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle a literal true", () => {
            let str = [
                'if true',
                'then go to next card'
            ].join('\n');
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle a literal true comparison", () => {
            let str = [
                'if myVariable is true',
                'then go to next card'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle literal true with not", () => {
            let str = [
                'if not true',
                'then go to next card'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle a literal false", () => {
            let str = [
                'if false',
                'then go to next card'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle a literal false comparison", () => {
            let str = [
                'if myVariable is false',
                'then go to next card'
            ].join('\n');
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle a literal false with not", () => {
            let str = [
                'if not false',
                'then go to next card'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Is recognized as a Statement", () => {
            let str = [
                'if myVariable',
                'then go to next card'
            ].join("\n");
            semanticMatchTest(str, "Statement");
        });
    });
    describe("With 'else'", () => {
        it("Can handle basic comparison", () => {
            let str = [
                'if 2 == 2',
                'then go to next card',
                'else put 5 into myVariable'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle comparison with variable", () => {
            let str = [
                'if myVariable > 35',
                'then put 35 into myVariable',
                'else put 1 into myVariable'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle a literal true", () => {
            let str = [
                'if true',
                'then go to next card',
                'else put false into myResult'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle a literal true with comparison", () => {
            let str = [
                'if myVariable is true',
                'then go to next card',
                'else go to previous card'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle literal true with not", () => {
            let str = [
                'if not true',
                'then go to previous card',
                'else go to next card'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle literal false", () => {
            let str = [
                'if false',
                'then go to previous card',
                'else go to next card'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle a literal false with comparison", () => {
            let str = [
                'if myVariable is false',
                'then go to previous card',
                'else go to next card'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Can handle literal false with not", () => {
            let str = [
                'if not false',
                'then go to previous card',
                'else put "hello" into myString'
            ].join("\n");
            semanticMatchTest(str, "IfThenSingleline");
        });
        it("Is recognized as a Statement", () => {
            let str = [
                'if myVariable',
                'then go to next card',
                'else go to previous card'
            ].join("\n");
            semanticMatchTest(str, "Statement");
        });
    });
});

describe("Basic IfThenMultiline", () => {
    describe("Without else", () => {
        it('Can handle basic comparison', () => {
            let str = [
                'if 2 == 2',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle comparison with a variable', () => {
            let str = [
                'if 2 == myVariable',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal true', () => {
            let str = [
                'if true',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal true comparison', () => {
            let str = [
                'if myVariable is true',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal true with not', () => {
            let str = [
                'if not true',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle literal false', () => {
            let str = [
                'if false',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal false with comparison', () => {
            let str = [
                'if myVariable is false',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle literal false with not', () => {
            let str = [
                'if not false',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Is recognized as a Statement', () => {
            let str = [
                'if true',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "Statement");
        });
    });
    describe('With else', () => {
        it('Can handle basic comparison', () => {
            let str = [
                'if 2 == 2',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
            semanticMatchTest(str, "IfThenMultiline_withElse");
        });
        it('Can handle comparison with a variable', () => {
            let str = [
                'if myVariable is 2',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal true', () => {
            let str = [
                'if true',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal true comparison', () => {
            let str = [
                'if myVariable is true',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal true with not', () => {
            let str = [
                'if not true',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal false', () => {
            let str = [
                'if false',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal false comparison', () => {
            let str = [
                'if myVariable is false',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Can handle a literal false with not', () => {
            let str = [
                'if not false',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "IfThenMultiline");
        });
        it('Is recognized as a Statement', () => {
            let str = [
                'if true',
                'then',
                'doSomething',
                'go to next card',
                'answer "You navigated"',
                'else',
                'doSomeOtherThing',
                'go to previous card',
                'end if'
            ].join('\n');
            semanticMatchTest(str, "Statement");
        });
    });
});


<|start_filename|>js/ohm/tests/test-property-value-grammar.js<|end_filename|>
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
};
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};

const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
};

describe("PropertyValue grammar tests", () => {
    it("Can parse a PropertyValue with implicit context", () => {
        let str = `the "name"`;
        semanticMatchTest(str, "PropertyValue");
        semanticMatchTest(str, "PropertyValue_withoutSpecifier");
    });
    it("Can match a PropertyValue with deep object specifier context", () => {
        let str = `the "name" of field 3 of second card of current stack`;
        semanticMatchTest(str, "PropertyValue");
        semanticMatchTest(str, "PropertyValue_withSpecifier");
    });
    it("Can parse a PropertyValue with 'this <partType>' context", () => {
        let str = `the "name" of this button`;
        semanticMatchTest(str, "PropertyValue");
        semanticMatchTest(str, "PropertyValue_withSpecifier");
    });
    it("Can parse a PropertyValue as a part of a Factored Expression", () => {
        let str = `(the "top" of this card + 5)`;
        semanticMatchTest(str, "Expression");
    });
});


<|start_filename|>js/objects/views/PartView.js<|end_filename|>
/**
 * PartView
 * ----------------------------------------
 * I am an *abstract* webcompoent CustomElement
 * that serves as the generic view for any Part
 * models.
 * I should not be instantiated directly, nor should
 * I be added to any web page's registry of CustomElements.
 * I am indended to be extended (subclassed) by the actual
 * views for each Part kind, and therefore I contain all
 * of the common behavior, including lifecycle methods,
 * for these.
 */
import ContextMenu from './contextmenu/ContextMenu.js';
import interpreterSemantics from '../../ohm/interpreter-semantics.js';

window.customElements.define('st-context-menu', ContextMenu);

class PartView extends HTMLElement {
    constructor(){
        super();
        this.model = null;
        this.isPartView = true;
        this.isLensed = false;
        this.name = this.constructor.name;
        this.propChangeHandlers = {};
        this.setupBasePropHandlers();
        this.viewChangeHandlers = {};
        this.setupBaseViewChangeHandlers();

        // Halo settings. All are on by default
        this.wantsHaloResize = true;
        this.wantsHaloRotate = true;
        this.wantsHaloScriptEdit = true;
        this.wantsHaloEdit = true;
        this.wantsHaloDelete = true;
        this.wantsHalo = true;
        // Note: see getter for wantsHaloMove

        // Context menu settings
        this.wantsContextMenu = true;

        // Bind component methods
        this.setModel = this.setModel.bind(this);
        this.unsetModel = this.unsetModel.bind(this);
        this.sendMessage = this.sendMessage.bind(this);
        this.setupBasePropHandlers = this.setupBasePropHandlers.bind(this);
        this.setupBaseViewChangeHandlers = this.setupBaseViewChangeHandlers.bind(this);
        this.initLayout = this.initLayout.bind(this);

        // Bind initial property method
        this.styleCSS = this.styleCSS.bind(this);
        this.styleTextCSS = this.styleTextCSS.bind(this);

        // Bind property change reaction methods
        this.primHandlePropChange = this.primHandlePropChange.bind(this);
        this.onPropChange = this.onPropChange.bind(this);
        this.primHandleViewChange = this.primHandleViewChange.bind(this);
        this.onViewChange = this.onViewChange.bind(this);
        this.scriptChanged = this.scriptChanged.bind(this);
        this.layoutChanged = this.layoutChanged.bind(this);
        this.listDirectionChanged = this.listDirectionChanged.bind(this);
        this.listWrappingChanged = this.listWrappingChanged.bind(this);
        this.vResizingChanged = this.vResizingChanged.bind(this);
        this.hResizingChanged = this.hResizingChanged.bind(this);
        this.pinningLeftChanged = this.pinningLeftChanged.bind(this);
        this.pinningTopChanged = this.pinningTopChanged.bind(this);
        this.pinningBottomChanged = this.pinningBottomChanged.bind(this);
        this.pinningRightChanged = this.pinningRightChanged.bind(this);
        this.listAlignmentChanged = this.listAlignmentChanged.bind(this);
        this.listDistributionChanged = this.listDistributionChanged.bind(this);

        // Bind view change reaction methods
        this.subpartOrderChanged = this.subpartOrderChanged.bind(this);
        this.newSubpartView = this.newSubpartView.bind(this);

        // Bind Halo related methods
        this.openHalo = this.openHalo.bind(this);
        this.closeHalo = this.closeHalo.bind(this);
        this.onHaloDelete = this.onHaloDelete.bind(this);
        this.onHaloOpenEditor = this.onHaloOpenEditor.bind(this);
        this.onHaloOpenScriptEditor = this.onHaloOpenScriptEditor.bind(this);
        this.onHaloResize = this.onHaloResize.bind(this);
        this.onHaloRotate = this.onHaloRotate.bind(this);
        this.onHaloPaste = this.onHaloPaste.bind(this);
        this.onHaloCopy = this.onHaloCopy.bind(this);
        this.onHaloTarget = this.onHaloTarget.bind(this);
        this.endHaloTarget = this.endHaloTarget.bind(this);
        this.onHaloTargetButtonMouseEnter = this.onHaloTargetButtonMouseEnter.bind(this);
        this.onHaloTargetButtonMouseLeave = this.onHaloTargetButtonMouseLeave.bind(this);
        this.onHaloActivationClick = this.onHaloActivationClick.bind(this);
        this.onHaloOpenEditor = this.onHaloOpenEditor.bind(this);
        this.onAuxClick = this.onAuxClick.bind(this);
        this.onClick = this.onClick.bind(this);
        this.onContextMenuClick = this.onContextMenuClick.bind(this);
        this.onMouseDown = this.onMouseDown.bind(this);
        this.onMouseUp = this.onMouseUp.bind(this);
        this.onMouseMove = this.onMouseMove.bind(this);
        this.handleTargetKey = this.handleTargetKey.bind(this);
        this.handleTargetMouseClick = this.handleTargetMouseClick.bind(this);
        this.handleTargetMouseOver = this.handleTargetMouseOver.bind(this);
        this.handleTargetMouseOut = this.handleTargetMouseLeave.bind(this);
        this.addContextMenuItems = this.addContextMenuItems.bind(this);
        this.getCurrentTargetViews = this.getCurrentTargetViews.bind(this);

        // Bind editor related methods
        this.openEditor = this.openEditor.bind(this);
        this.closeEditor = this.closeEditor.bind(this);

        // Context menu
        this.openContextMenuAt = this.openContextMenuAt.bind(this);
        this.closeContextMenu = this.closeContextMenu.bind(this);

        // misc
        this.highlight = this.highlight.bind(this);
        this.unhighlight = this.unhighlight.bind(this);

        // Bind lifecycle methods
        this.afterModelSet = this.afterModelSet.bind(this);
        this.afterModelUnset = this.afterModelUnset.bind(this);
        this.afterConnected = this.afterConnected.bind(this);
        this.afterDisconnected = this.afterDisconnected.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            // Do some universal PartView configuration
            // when attached to a parent element, like
            // registering event listeners etc

            // Register middle mouse button click
            // to toggle the halo
            this.addEventListener('auxclick', this.onAuxClick);

            // Register default event handlers manually]
            this.addEventListener('click', this.onClick);
            this.addEventListener('contextmenu', this.onContextMenuClick);

            // Call the lifecycle method when done
            // with the above
            this.afterConnected();
        }
    }

    disconnectedCallback(){
        this.removeEventListener('auxclick', this.onAuxClick);
        this.removeEventListener('click', this.onClick);
        this.removeEventListener('contextmenu', this.onContextMenuClick);
        this.afterDisconnected();
    }

    setModel(aModel){
        this.unsetModel();
        this.model = aModel;
        aModel.addPropertySubscriber(this);
        aModel.addViewSubscriber(this);
        if(this.isLensed){
            this.removeAttribute('part-id');
            this.setAttribute('lens-part-id', aModel.id);
        } else {
            this.removeAttribute('lens-part-id');
            this.setAttribute('part-id', aModel.id);
        }

        // load all the initial styling
        this.styleCSS();
        this.styleTextCSS();
        this.initLayout();

        // deal with 'special' props
        let wantsMove = this.model.partProperties.getPropertyNamed(
            this.model,
            "wants-move"
        );
        if(wantsMove){
            this.addEventListener('mousedown', this.onMouseDown);
        }

        let haloOpen = this.model.partProperties.findPropertyNamed(
            "halo-open"
        );
        if(haloOpen && haloOpen._value){
            this.openHalo();
        }

        this.afterModelSet();
    }

    unsetModel(){
        if(this.model){
            let removedModel = this.model;
            this.model.removePropertySubscriber(this);
            this.model = null;
            this.setAttribute('part-id', "");
            this.afterModelUnset(removedModel);
        }
    }

    setupBasePropHandlers(){
        // This is where we should setup any
        // prop change handlers that are universal
        // to all PartViews. We would do this via
        // the #onPropChange method, which registers
        // a handler function.
        // Do not override this method
        // TODO: Implement the universals
        this.onPropChange('script', this.scriptChanged);
        this.onPropChange('number', this.numberChanged);
        this.onPropChange('cssStyle', this.styleCSS);
        this.onPropChange('cssTextStyle', this.styleTextCSS);
        this.onPropChange('layout', this.layoutChanged);
        this.onPropChange('list-direction', this.listDirectionChanged);
        this.onPropChange('list-wrapping', this.listWrappingChanged);
        this.onPropChange('list-alignment', this.listAlignmentChanged);
        this.onPropChange('list-distribution', this.listDistributionChanged);
        this.onPropChange('horizontal-resizing', this.hResizingChanged);
        this.onPropChange('vertical-resizing', this.vResizingChanged);
        this.onPropChange('pinning-top', this.pinningTopChanged);
        this.onPropChange('pinning-right', this.pinningRightChanged);
        this.onPropChange('pinning-left', this.pinningLeftChanged);
        this.onPropChange('pinning-bottom', this.pinningBottomChanged);
        this.onPropChange('wants-move', (value) => {
            if(value){
                this.addEventListener('mousedown', this.onMouseDown);
            } else {
                this.removeEventListener('mousedown', this.onMouseDown);
            }
        });
        this.onPropChange('halo-open', (value) => {
            if(value){
                this.openHalo();
            } else {
                this.closeHalo();
            }
        });
        this.onPropChange('editor-open', (value) => {
            if(value){
                this.openEditor();
            } else {
                this.closeEditor();
            }
        });
    }

    setupBaseViewChangeHandlers(){
        // This is where we should setup any
        // view change handlers that are universal
        // to all PartViews. We would do this via
        // the #onViewChange method, which registers
        // a handler function.
        // Do not override this method
        this.onViewChange('subpart-order', this.subpartOrderChanged);
        this.onViewChange('subpart-new', this.newSubpartView);
    }

    initLayout(){
        // Not all Part/PartView pairs have the layout
        // properties. Ensure they exist first
        let hasLayout = this.model.partProperties.findPropertyNamed('layout');
        let hasBoxResizing = this.model.partProperties.findPropertyNamed('vertical-resizing');
        let hasPinning = this.model.partProperties.findPropertyNamed('pinning');
        if(hasLayout){
            let initialLayout = this.model.partProperties.getPropertyNamed(
                this.model,
                'layout'
            );
            let initialListDirection = this.model.partProperties.getPropertyNamed(
                this.model,
                'list-direction'
            );
            let initialListWrapping = this.model.partProperties.getPropertyNamed(
                this.model,
                'list-wrapping'
            );
            this.layoutChanged(initialLayout);
            this.listDirectionChanged(initialListDirection);
            this.listWrappingChanged(initialListWrapping);
            this.listAlignmentChanged();
            this.listDistributionChanged();
        }

        if(hasBoxResizing){
            let initialVResizing = this.model.partProperties.getPropertyNamed(
                this.model,
                'vertical-resizing'
            );
            let initialHResizing = this.model.partProperties.getPropertyNamed(
                this.model,
                'horizontal-resizing'
            );
            this.vResizingChanged(initialVResizing);
            this.hResizingChanged(initialHResizing);
        }

        if(hasPinning){
            this.pinningTopChanged();
            this.pinningBottomChanged();
            this.pinningLeftChanged();
            this.pinningRightChanged();
        }
    }

    styleCSS(){
        let cssStyle = this.model.partProperties.getPropertyNamed(this, "cssStyle");
        Object.keys(cssStyle).forEach((key) => {
            let value = cssStyle[key];
            this.style[key] = value;
        });
    }

    styleTextCSS(){
        let cssStyle = this.model.partProperties.getPropertyNamed(this, "cssTextStyle");
        Object.keys(cssStyle).forEach((key) => {
            let value = cssStyle[key];
            this.style[key] = value;
        });
    }

    sendMessage(aMessage, target){
        if(!this.isLensed){
            // Lensed views should not send messages
            window.System.sendMessage(aMessage, this, target);
        }
    }

    receiveMessage(aMessage){
        switch(aMessage.type){
        case 'propertyChanged':
            this.primHandlePropChange(
                aMessage.propertyName,
                aMessage.value,
                aMessage.partId
            );
            break;
        case 'viewChanged':
            this.primHandleViewChange(
                aMessage.changeName,
                ...aMessage.args
            );
            break;
        }
    }

    primHandlePropChange(name, value, partId){
        // We notify the model that the property change so that
        // on propertyChanged command handlers could be invoked
        // but we make sure that this stops at the said model and
        // does not go up the delegation chain
        let commandMessage = {
            type: 'command',
            commandName: 'propertyChanged',
            args: [name, value],
            shouldNotDelegate:true, // do not send this up the delegation chain
            shouldIgnore: true
        };
        this.sendMessage(commandMessage, this.model);
        // Find the handler for the given named
        // property. If it does not exist, do nothing
        let handler = this.propChangeHandlers[name];
        if(!handler){
            return null;
        }
        handler = handler.bind(this);
        return handler(value, partId);
    }


    onPropChange(name, func){
        this.propChangeHandlers[name] = func;
    }

    primHandleViewChange(name, ...args){
        // Find the handler for the given named
        // property. If it does not exist, do nothing
        let handler = this.viewChangeHandlers[name];
        if(!handler){
            return null;
        }
        handler = handler.bind(this);
        return handler(...args);
    }

    onViewChange(name, func){
        this.viewChangeHandlers[name] = func;
    }

    scriptChanged(value, partId){
        // make sure that we are only sending the compile
        // message when dealing with a 'core' st-part views (not
        // with navigator or related views)
        if(this.tagName.split("-")[0] != "ST" || this.slot == "wrapped-view"){
            return;
        }
        this.model.sendMessage({
            type: 'compile',
            codeString: value,
            targetId: partId
        }, window.System);
    }

    subpartOrderChanged(id, currentIndex, newIndex){
        // there is no need to do anything for the wrapped views
        // CardRow and StackRow will handle the updates
        if(this.name == "WrappedView"){
            return;
        }
        let subpartNode = this.childNodes[currentIndex];
        if(newIndex == this.childNodes.length - 1){
            this.appendChild(subpartNode);
        } else {
            // we need to account for whether the index of this
            // is before or after the newIndex
            if(currentIndex < newIndex){
                newIndex = newIndex + 1;
            }
            let referenceNode = this.childNodes[newIndex];
            this.insertBefore(subpartNode, referenceNode);
        }
    }

    newSubpartView(newView){
        this.appendChild(newView);
    }

    layoutChanged(value, partId){
        if(value == 'list'){
            this.classList.add('list-layout');
        } else {
            this.classList.remove('list-layout');
        }
    }

    listDirectionChanged(value, partId){
        // Row is the default configuration
        // for a list layout, so only one extra
        // CSS class needs to be toggled
        if(value == 'row'){
            this.classList.remove('list-column');
        } else if(value == 'column'){
            this.classList.add('list-column');
        }
    }

    listWrappingChanged(value, partId){
        if(value == true){
            this.classList.add('wrap-list');
        } else {
            this.classList.remove('wrap-list');
        }
    }

    hResizingChanged(value){
        if(value == 'space-fill'){
            this.classList.add('h-space-fill');
            this.classList.remove(
                'h-rigid',
                'h-shrink-wrap'
            );
        } else if(value == 'shrink-wrap'){
            this.classList.add('h-shrink-wrap');
            this.classList.remove(
                'h-rigid',
                'h-space-fill'
            );
        } else if(value == 'rigid'){
            this.classList.add('h-rigid');
            this.classList.remove(
                'h-space-fill',
                'h-shrink-wrap'
            );
        }
    }

    vResizingChanged(value){
        if(value == 'space-fill'){
            this.classList.add('v-space-fill');
            this.classList.remove(
                'v-rigid',
                'v-shrink-wrap'
            );
        } else if(value == 'shrink-wrap'){
            this.classList.add('v-shrink-wrap');
            this.classList.remove(
                'v-rigid',
                'v-space-fill'
            );
        } else if(value == 'rigid'){
            this.classList.add('v-rigid');
            this.classList.remove(
                'v-space-fill',
                'v-shrink-wrap'
            );
        }
    }

    pinningTopChanged(){
        let top = this.model.partProperties.getPropertyNamed(
            this.model,
            'pinning-top'
        );
        if(top){
            this.classList.add('pin-top');
        } else {
            this.classList.remove('pin-top');
        }
    }

    pinningLeftChanged(){
        let left = this.model.partProperties.getPropertyNamed(
            this.model,
            'pinning-left'
        );
        if(left){
            this.classList.add('pin-left');
        } else {
            this.classList.remove('pin-left');
        }
    }

    pinningRightChanged(){
        let right = this.model.partProperties.getPropertyNamed(
            this.model,
            'pinning-right'
        );
        if(right){
            this.classList.add('pin-right');
        } else {
            this.classList.remove('pin-right');
        }
    }

    pinningBottomChanged(){
        let bottom = this.model.partProperties.getPropertyNamed(
            this.model,
            'pinning-bottom'
        );
        if(bottom){
            this.classList.add('pin-bottom');
        } else {
            this.classList.remove('pin-bottom');
        }
    }

    listAlignmentChanged(){
        let value = this.model.partProperties.getPropertyNamed(
            this.model,
            'list-alignment'
        );
        let valid = [
            'top',
            'bottom',
            'left',
            'right',
            'center'
        ];
        if(valid.includes(value)){
            valid.forEach(side => {
                this.classList.remove(`list-align-${side}`);
            });
            this.classList.add(`list-align-${value}`);
        }
    }

    listDistributionChanged(){
        let value = this.model.partProperties.getPropertyNamed(
            this.model,
            'list-distribution'
        );
        let valid = [
            'start',
            'end',
            'space-between',
            'space-around',
            'center'
        ];
        if(valid.includes(value)){
            valid.forEach(side => {
                this.classList.remove(`list-distribution-${side}`);
            });
            this.classList.add(`list-distribution-${value}`);
        }
    }

    /* Lifecycle Method Defaults */
    afterModelSet(){
        // Does nothing.
        // Should be implemented in subclasses
    }

    afterModelUnset(removedModel){
        // Does nothing.
        // Should be implemented in subclasses
    }

    afterConnected(){
        // Does nothing by default.
        // Should be implemented in subclass
    }

    afterDisconnected(){
        // Does nothing by default.
        // Should be implemented in subclass
    }

    /* Halo Related Methods */

    openHalo(){
        // Check to see if there's a halo in
        // the component's shadow root already
        let foundHalo = this.shadowRoot.querySelector('st-halo');
        if(!foundHalo){
            let newHalo = document.createElement('st-halo');
            this.shadowRoot.appendChild(newHalo);
        }
    }

    closeHalo(){
        let foundHalo = this.shadowRoot.querySelector('st-halo');
        if(foundHalo){
            foundHalo.remove();
        }
    }

    toggleAntsBorder(){
        if(this.classList.contains('marching-ants')){
            this.classList.remove('marching-ants');
        } else {
            this.classList.add('marching-ants');
        }
    }

    onHaloDelete(){
        // What to do when the user clicks the
        // delete button on a halo for this partview.
        // The default implementation is to send a message
        // to the System to delete the corresponding
        // model and *all* views referencing that
        // model.
        this.sendMessage({
            type: 'command',
            commandName: 'deleteModel',
            args: [this.model.id]
        }, this.model);
    }

    onHaloOpenScriptEditor(){
        // Send the message to open a script editor
        // with this view's model as the target
        this.model.sendMessage({
            type: 'command',
            commandName: 'openScriptEditor',
            args: [this.model.id]
        }, this.model);
    }

    onHaloOpenEditor(){
        this.model.partProperties.setPropertyNamed(this.model, "editor-open", true);
    }

    onHaloResize(movementX, movementY){
        // Default implementation on what to do during
        // halo button resize opertations. Subclasses
        // can override for custom behavior.
        // Default is to update the View component's
        // width and height style properties directly.
        // If the part is rotated this will throw off the bounding rectangle
        // browser calcualtion. So the hack here is to rotate the part to 0
        // (if necessary) do the calculations and then rotate it back
        let angle = this.model.partProperties.getPropertyNamed(this.model, "rotate");
        if(angle){
            this.model.partProperties.setPropertyNamed(this.model, "rotate", 0);
        }
        let rect = this.getBoundingClientRect();
        let newWidth, newHeight;
        if(this.preserveAspectOnResize){
            let ratio = rect.width / rect.height;
            let hyp = Math.sqrt((movementX**2) + (movementY**2));
            if(movementX < 0 || movementY < 0){
                hyp = hyp * -1;
            }
            newHeight = rect.height + hyp;
            newWidth = rect.width + hyp;
        } else {
            newWidth = movementX + rect.width;
            newHeight = movementY + rect.height;
        }
        this.model.partProperties.setPropertyNamed(this.model, "width", newWidth);
        this.model.partProperties.setPropertyNamed(this.model, "height", newHeight);
        // reset the rotate angle to the original (if necessary)
        if(angle){
            this.model.partProperties.setPropertyNamed(this.model, "rotate", angle);
        }
    }

    onHaloRotate(movementX, movementY){
        // Default implementation on what to do during
        // halo button rotate opertations. Subclasses
        // can override for custom behavior.
        // Default is to update the View component's
        // rotate style property directly.
        if(movementX || movementY){
            let currentAngle = this.model.partProperties.getPropertyNamed(this.model, "rotate");
            let rect = this.getBoundingClientRect();
            if(!currentAngle){
                currentAngle = 0;
            }
            let theta1 = Math.atan((rect.height/2)/(rect.width/2));
            let theta2 = Math.atan((rect.height/2 + movementY)/(rect.width/2 + movementX));
            let changeAngle = Math.abs((theta2 - theta1)*180/Math.PI);
            let newAngle = (currentAngle + changeAngle) % 360;
            if(newAngle < 0){
                newAngle = 360 + newAngle;
            }
            if(newAngle){
                this.model.partProperties.setPropertyNamed(this.model, "rotate", newAngle);
            }
        }
    }

    onHaloCopy(){
        window.System.clipboard.copyPart(this.model);
    }

    onHaloPaste(){
        window.System.clipboard.pasteContentsInto(this.model);
        this.model.partProperties.setPropertyNamed(this.model, "halo-open", false);
    }

    onHaloTarget(event){
        // Add targeting receive listeners to all PartViews
        // on the current card.
        let currentStackView = document.querySelector(`[part-id="${window.System.world.currentStack.id}"]`);
        let currentCardView = document.querySelector(`[part-id="${window.System.world.currentStack.currentCard.id}"]`);
        let targetCardParts = Array.from(currentCardView.querySelectorAll('[part-id]'));
        let targetStackParts = Array.from(currentStackView.querySelectorAll('[part-id]:not(st-card):not(st-stack)'));
        let allTargets = targetCardParts.concat(targetStackParts);
        allTargets.forEach(partView => {
            document.addEventListener('keydown', this.handleTargetKey);
            partView.addEventListener('mouseover', this.handleTargetMouseOver);
            partView.addEventListener('mouseout', this.handleTargetMouseOut);
            partView.addEventListener('click', this.handleTargetMouseClick);
        });
        document.body.classList.add('targeting-mode');
        event.stopPropagation();
    }

    onHaloTargetButtonMouseEnter(){
        // light up the current target
        this.getCurrentTargetViews().forEach((view) => {
            view.highlight("rgb(54, 172, 100)"); //green
        });
    }

    onHaloTargetButtonMouseLeave(){
        // light up the current target
        this.getCurrentTargetViews().forEach((view) => {
            view.unhighlight();
        });
    }

    highlight(color){
        if(this.name != "StackView" && this.name != "WorldView"){
            this._tempBackgroundColor = this.model.partProperties.getPropertyNamed(this.model, "background-color");
            this.model.partProperties.setPropertyNamed(this.model, "background-color", color);
            this._tempBackgroundTransparency = this.model.partProperties.getPropertyNamed(this.model, "background-transparency");
            this.model.partProperties.setPropertyNamed(this.model, "background-transparency", 1);
        }

    }

    unhighlight(){
        if(this.name != "StackView" && this.name != "WorldView"){
            this.model.partProperties.setPropertyNamed(this.model, "background-color", this._tempBackgroundColor);
            this.model.partProperties.setPropertyNamed(this.model, "background-transparency", this._tempBackgroundTransparency);
        }
    }

    endHaloTarget(){
        // Remove all targeting related event listeners
        // that were added during the onHaloTarget
        // handler
        let currentStackView = document.querySelector(`[part-id="${window.System.world.currentStack.id}"]`);
        let currentCardView = document.querySelector(`[part-id="${window.System.world.currentStack.currentCard.id}"]`);
        let targetCardParts = Array.from(currentCardView.querySelectorAll('[part-id]'));
        let targetStackParts = Array.from(currentStackView.querySelectorAll('[part-id]:not(st-card):not(st-stack)'));
        let allTargets = targetCardParts.concat(targetStackParts);
        allTargets.forEach(partView => {
            document.removeEventListener('keydown', this.handleTargetKey);
            partView.removeEventListener('keydown', this.handleTargetKey);
            partView.removeEventListener('mouseover', this.handleTargetMouseOver);
            partView.removeEventListener('mouseout', this.handleTargetMouseOut);
            partView.removeEventListener('click', this.handleTargetMouseClick);
        });
        document.body.classList.remove('targeting-mode');
    }

    handleTargetKey(event){
        if(event.key == 'Escape'){
            this.endHaloTarget();
        }
    }

    handleTargetMouseOver(event){
        if(!event.target.classList.contains('targeting')){
            event.target.classList.add('targeting');
            event.target.highlight("rgb(234, 55, 55)");
            event.target.removeEventListener('click', event.target.onClick);
        }
    }

    handleTargetMouseLeave(event){
        if(event.target.classList.contains('targeting')){
            event.target.classList.remove('targeting');
            event.target.unhighlight();
            event.target.addEventListener('click', event.target.onClick);
        }
    }

    handleTargetMouseClick(event){
        event.preventDefault();
        if(event.button == 0 && event.shiftKey){
            this.onHaloActivationClick(event);
            return;
        }
        event.target.classList.remove('targeting');
        this.model.partProperties.setPropertyNamed(
            this.model,
            'target',
            event.target.model.id
        );
        this.endHaloTarget();
        event.stopImmediatePropagation();
        event.target.unhighlight();
        event.target.addEventListener('click', event.target.onClick);
    }

    getCurrentTargetViews(){
        // clean up the current target
        let currentTarget = this.model.partProperties.getPropertyNamed(this.model, "target");
        if(currentTarget){
            let semantics = window.System.grammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(this.model, window.System)
            );
            let m = window.System.grammar.match(currentTarget, "ObjectSpecifier");
            let targetId = semantics(m).interpret();
            return window.System.findViewsById(targetId);
        }
        return [];
    }

    onContextMenuClick(event){
        if(this.wantsContextMenu){
            event.preventDefault();
            event.stopPropagation();
            if(this.contextMenuIsOpen){
                this.closeContextMenu();
            } else {
                this.openContextMenuAt(
                    event.clientX,
                    event.clientY
                );
            }
        } else {
            event.stopPropagation();
        }
    }

    onAuxClick(event){
        // Should only open halo when middle
        // mouse button is clicked
        if(event.button == 1){
            event.preventDefault();
            this.onHaloActivationClick(event);
        }
    }

    onClick(event){
        if(this.contextMenuIsOpen){
            this.closeContextMenu();
        }
        if(event.button == 0 && event.shiftKey){
            event.preventDefault();
            this.onHaloActivationClick(event);
        }
    }

    onHaloActivationClick(event){
        if(this.wantsHalo){
            if(this.hasOpenHalo){
                this.model.partProperties.setPropertyNamed(this.model, "halo-open", false);
            } else {
                event.stopPropagation();
                this.model.partProperties.setPropertyNamed(this.model, "halo-open", true);
            }
        }
    }

    onMouseDown(event){
        if(event.button == 0 && !event.shiftKey){
            document.addEventListener('mousemove', this.onMouseMove);
            document.addEventListener('mouseup', this.onMouseUp);
        }
    }

    onMouseMove(event){
        this.sendMessage({
            type: 'command',
            commandName: 'move',
            args: [event.movementX, event.movementY]
        }, this.model);
    }

    onMouseUp(event){
        document.removeEventListener('mousemove', this.onMouseMove);
        document.removeEventListener('mouseup', this.onMouseUp);
    }

    openContextMenuAt(x, y){
        let menuEl = document.createElement('st-context-menu');
        menuEl.render(this.model);
        menuEl.style.left = `${x}px`;
        menuEl.style.top = `${y}px`;
        document.body.append(menuEl);

        // Ensure that the menu is completely
        // within the current view. If not (meaning
        // it is clipped), then adjust so it is
        // completely in the view.
        menuEl.adjustToClientView();
    }

    closeContextMenu(){
        let found = document.querySelector('st-context-menu');
        if(found){
            found.remove();
        }
    }

    addContextMenuItems(contextMenu){
        // The default implementation is to
        // do nothins.
        // Subclasses should override and use the
        // passed-in contextMenu object to construct
        // list items that are specific to their needs
        return;
    }

    get wantsHaloMove(){
        if(!this.parentElement || !this.isConnected){
            return false;
        }
        let parentModel = this.parentElement.model;
        if(!parentModel){
            return true;
        }

        let hasLayout = parentModel.partProperties.findPropertyNamed(
            parentModel,
            'layout'
        );

        if(!hasLayout){
            return true;
        }

        let parentLayout = parentModel.partProperties.getPropertyNamed(
            parentModel,
            'layout'
        );
        if(parentLayout === 'strict' | !parentLayout || parentLayout == ""){
            return true;
        }

        return false;
    }

    get contextMenuIsOpen(){
        let found = document.querySelector('st-context-menu');
        if(found){
            return true;
        }
        return false;
    }

    /* Editor related methods */
    openEditor(){
        let editor = document.querySelector('st-editor');
        editor.render(this.model);
        if(!editor.isOpen){
            editor.open();
        }
    }

    closeEditor(){
        let editor = document.querySelector('st-editor.open');
        if(editor){
            editor.close();
        }
    }
};

export {
    PartView,
    PartView as default
};


<|start_filename|>js/objects/parts/Audio.js<|end_filename|>
import {Part} from './Part.js';
import {
    addBasicStyleProps,
    addPositioningStyleProps,
    addTextStyleProps
} from '../utils/styleProperties.js';

class Audio extends Part {
    constructor(owner, src) {
        super(owner);

        // Properties
        this.partProperties.newBasicProp(
            "src",
            null
        );

        this.src = null;

        this.partProperties.newBasicProp(
            'readyState',
            "HAVE_NOTHING"
        );

        this.partProperties.newBasicProp(
            "play",
            false
        );

        this.partProperties.newBasicProp(
            "stop",
            null
        );

        // Private command handlers
        this.setPrivateCommandHandler("loadAudioFromSource", this.loadAudioFromSource);
        this.setPrivateCommandHandler("play", () => {this.play(true);});
        this.setPrivateCommandHandler("pause", () => {this.play(false);});
        this.setPrivateCommandHandler("stop", this.stop);

        // Bind component methods
        this.loadAudioFromSource = this.loadAudioFromSource.bind(this);
        this.play = this.play.bind(this);
        this.stop = this.stop.bind(this);


        // load the src if provided
        if(src){
            this.partProperties.setPropertyNamed(this, "src", url);
        }
        // Style properties
        addBasicStyleProps(this);
        addPositioningStyleProps(this);
        addTextStyleProps(this);
        this.setupStyleProperties();
        this.partProperties.setPropertyNamed(
            this,
            'background-transparency',
            0,
            true, // notify
            true // set default
        );
        ["right", "left", "top", "bottom"].forEach((side) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${side}-width`,
                1,
                true, // notify
                true // set default
            );
        });
    }

    get type(){
        return 'audio';
    }

    loadAudioFromSource(senders, sourceUrl){
        this.partProperties.setPropertyNamed(this, "src", sourceUrl);
    }

    play(value){
        this.partProperties.setPropertyNamed(this, "play", value);
        this.partProperties.setPropertyNamed(this, "stop", false);
    }

    stop(){
        this.partProperties.setPropertyNamed(this, "play", false);
        this.partProperties.setPropertyNamed(this, "stop", true);
    }
};

export {
    Audio,
    Audio as default
};


<|start_filename|>js/objects/tests/test-system-script-editor-with-preload.js<|end_filename|>
/**
 * System Script Editor Window Tests
 * -------------------------------------
 * These are integration tests that ensure we
 * are creating, properly opening, and have
 * correct functionality in the demo Script
 * Editor window.
 * To open a script editor, we send the
 * 'openScriptEditor' command to System with
 * the ID of the target Part (whose script we
 * will edit) as the sole arg.
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

// We will use the following as the default script
// on the Card that we test. That way, we can see if
// the ScriptEditor opens with existing correct script
// text
let originalScript = `on click\n\tanswer "hello"\nend click`;


describe('Opening a Script Editor', () => {

    it('Can remove the current window from the world for further testing (TODO! make programmatic) ', () => {
        let foundWindows = document.querySelectorAll('st-window');
        foundWindows.forEach((node) => {node.remove()});
        foundWindows = document.querySelectorAll('st-window');
        assert.equal(0, foundWindows.length);
    });
    it('Can set the script of the current card without issue', () => {
        let currentCard = document.querySelector('.current-stack .current-card').model;
        currentCard.partProperties.setPropertyNamed(
            currentCard,
            'script',
            originalScript
        );

        let foundScript = currentCard.partProperties.getPropertyNamed(
            currentCard,
            'script'
        );

        assert.equal(originalScript, foundScript);
    });
    it('Can open a script editor with current card as target', () => {
        let currentCard = document.querySelector('.current-stack .current-card').model;
        let message = {
            type: 'command',
            commandName: 'openScriptEditor',
            args: [currentCard.id]
        };

        let sendMessageFunc = function(){
            currentCard.sendMessage(message, System);
        };

        expect(sendMessageFunc).to.not.throw(Error);
    });
    it('Current CardView now has WindowView as a child element', () => {
        let currCardView = document.querySelector('.current-card');
        assert.exists(currCardView);
        let windowView = document.querySelector('st-window');
        assert.exists(windowView);

        let foundWindow = document.querySelector('st-window');
        assert.equal(foundWindow, windowView);
    });
    it('Current Card Part now has a Window as a subpart', () => {
        let currentCard = document.querySelector('st-card.current-card').model;
        assert.exists(currentCard);
        let windowModel = document.querySelector('st-window').model;
        assert.exists(windowModel);

        assert.include(currentCard.subparts, windowModel);
    });
    it('Window model is targeting the current card', () => {
        let winModel = document.querySelector('st-window').model;
        let cardModel = document.querySelector('.current-stack .current-card').model;
        let foundTarget = winModel.target;

        assert.equal(foundTarget, cardModel);
    });
});

describe('ScriptEditor Functionality', () => {
    let editorAreaView;
    let editorSaveButtonView;
    let editorFieldView;
    before(() => {
        editorAreaView = document.querySelector('st-window > st-area');
        editorSaveButtonView = editorAreaView.querySelector('st-button');
        editorFieldView = editorAreaView.querySelector('st-field');
    });
    it('Has all appropriate views', () => {
        assert.exists(editorAreaView);
        assert.exists(editorSaveButtonView);
        assert.exists(editorFieldView);
    });
    it('Editor has the current script html for the Card in its field', () => {
        let textArea = editorFieldView._shadowRoot.querySelector('.field-textarea');
        assert.exists(textArea);
        let displayedScriptHTML = textArea.innerHTML;

        let cardModel = document.querySelector('.current-stack > .current-card').model;
        let cardScript = cardModel.partProperties.getPropertyNamed(
            cardModel,
            'script'
        );

        assert.equal(editorFieldView.htmlToText(textArea), cardScript);
    });
    it('Editor has the current script text for the Card in its field', () => {
        let fieldModel = editorFieldView.model;
        let textArea = editorFieldView._shadowRoot.querySelector('.field-textarea');
        assert.exists(textArea);
        let text = fieldModel.partProperties.getPropertyNamed(
            fieldModel,
            'text'
        );

        let cardModel = document.querySelector('.current-stack > .current-card').model;
        let cardScript = cardModel.partProperties.getPropertyNamed(
            cardModel,
            'script'
        );
    });
    it('Can set a new script for the Card by changing field value and triggering button', () => {
        let newScript = `on foo\n\tanswer "foo"\n end foo`;
        let cardModel = document.querySelector('.current-stack > .current-card').model;
        let fieldModel = editorFieldView.model;
        let saveButtonModel = editorSaveButtonView.model;

        let textArea = editorFieldView._shadowRoot.querySelector('.field-textarea');
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'innerHTML',
            newScript
        );
        // due to JSDOM weirdness where element.innerText is not auto set if element.innerHTML is
        // we need to set the 'text' property again without notification
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'text',
            newScript,
            false
        );
        // Send click on the button,
        // which should itself send a message
        // to set the script of the card
        saveButtonModel.sendMessage({
            type: 'command',
            commandName: 'click',
            args: []
        }, saveButtonModel);

        let cardScript = cardModel.partProperties.getPropertyNamed(
            cardModel,
            'script'
        );

        assert.equal(cardScript, newScript);
    });
    describe('Simpletalk Completer', () => {
        before(() => {
            // make sure that the simpleTalkCompleter is set
            editorFieldView.editorCompleter = editorFieldView.simpleTalkCompleter;
        });
        it('Will complete "on messageName{SPACE}" with template', () => {
            let fieldModel = editorFieldView.model;
            let textArea = editorFieldView._shadowRoot.querySelector('.field-textarea');
            let newHTMLContent = "on message ";
            let completedHTMLContent = "on message <div><div>\t</div><div>end message</div><br></div>";

            // Simulate typing the input events
            let event = new window.Event('input');
            textArea.innerHTML = newHTMLContent;
            textArea.dispatchEvent(event);

            let foundHTMLContent = fieldModel.partProperties.getPropertyNamed(
                fieldModel,
                'innerHTML'
            );
            assert.equal(foundHTMLContent, completedHTMLContent);
        });
        it('Will complete "on messageName{NEWLINE}" with template', () => {
            let fieldModel = editorFieldView.model;
            let textArea = editorFieldView._shadowRoot.querySelector('.field-textarea');
            let newHTMLContent = "on message\n";
            let completedHTMLContent = "on message\nend message";

            // Simulate typing the input events
            let event = new window.Event('input');
            textArea.innerHTML = newHTMLContent;
            textArea.dispatchEvent(event);

            let foundHTMLContent = fieldModel.partProperties.getPropertyNamed(
                fieldModel,
                'innerHTML'
            );
            assert.equal(foundHTMLContent, completedHTMLContent);
        });
    });
});


<|start_filename|>js/objects/utils/errorHandler.js<|end_filename|>
/**
 * Error Handler
 * ------------------------------------
 * I am responsible for handler all
 * System-wide errors
 */

const errorHandler = {

    handle: function(aMessage){
        switch(aMessage.name){
            case 'GrammarMatchError':
                return this.handleGrammarMatchError(aMessage);
            case 'MessageNotUnderstood':
                return this.handleMessageNotUnderstood(aMessage);
            default:
                // if I don't know what to do with this message
                // I send it along to the System
                return window.System.receiveMessage(aMessage);
        }
    },

    handleGrammarMatchError: function(aMessage){
        // TODO is there a more structured way to get this out of ohm?
        let regex = /Line (?<line>\d), col (?<column>\d)/;
        let match = aMessage.parsedScript.message.match(regex);
        let errorLineNum = parseInt(match.groups["line"]) - 1; // ohm lines start with 1 
        // see if the grammar rule has been identified
        let ruleName;
        let rightMostFailures = aMessage.parsedScript.getRightmostFailures();
        if(rightMostFailures[1]){
            ruleName = rightMostFailures[1].pexpr.ruleName;
        }
        // get some more info about what the parser expected
        let expectedText = aMessage.parsedScript.getExpectedText();
        // get the original script
        let text = aMessage.parsedScript.input;
        let textLines = text.split("\n");
        // replace said text line with an error marker
        textLines[errorLineNum] += ` --<<<[Expected:${expectedText}; ruleName: "${ruleName}"]`;
        text = textLines.join("\n");
        // if the first message in the parsed script is "doIt" then the statementLines are
        // located in the corresponding field text, not the script, property and
        // we want the error to be marked up in the field textarea
        if(aMessage.parsedScript.input.startsWith("on doIt")){
            let originalSenderModel = window.System.partsById[aMessage.partId];
            // we need to get the original text so as not to completely replace it
            // then insert the markup in the appropriate line
            let fieldText = originalSenderModel.partProperties.getPropertyNamed(originalSenderModel, "text");
            let script = aMessage.parsedScript.input;
            script = this._cleanDoItSCript(script);
            // we don't want the "doIt" handler inserted back in, since it's just a hidden wrapper for the
            // statement lines
            text = this._cleanDoItSCript(text);
            fieldText = fieldText.replace(script, text);
            originalSenderModel.partProperties.setPropertyNamed(originalSenderModel, "text", fieldText);
        } else {
            // first locate the script editor in question
            let scriptEditor = window.System.findScriptEditorByTargetId(aMessage.partId);
            if(!scriptEditor){
                this._openScriptEditor(aMessage.partId);
                scriptEditor = window.System.findScriptEditorByTargetId(aMessage.partId);
            }
            scriptEditor.model.partProperties.setPropertyNamed(scriptEditor.model, "text", text);
        }
        // open the grammar if there is not one open already
        let currentCard = window.System.getCurrentCardModel();
        let grammar = currentCard.subparts.filter((part) => {
            return (part.type == "field") && (part.partProperties.getPropertyNamed(part, "name") == "SimpleTalk");
        });
        if(grammar.length == 0){
            this._openGrammar(aMessage.partId, ruleName);
        }
    },

    handleMessageNotUnderstood(aMessage){
        let offendingMessage = aMessage.message;
        let originalSender = offendingMessage.senders[0];
        // Are we ever going to have MNU errors on messages that
        // are not type: command?
        if(offendingMessage.type === "command"){
            let commandName = offendingMessage.commandName;
            let originalSenderModel = window.System.partsById[originalSender.id];
            let regex = new RegExp(`\\s*${commandName}(\s|\n|$)`, 'g');
            let text;
            let target;
            let executionStack = window.System.executionStack._stack;
            // if the first message in the execution stack is "doIt" then the statementLines are
            // located in the corresponding field text, not the script, property and
            // we want the error to be marked up in the field textarea
            if(executionStack[0] && executionStack[0].messageName == "doIt"){
                target = executionStack[0].part;
                text = target.partProperties.getPropertyNamed(originalSenderModel, 'text');
            } else {
                text = originalSenderModel.partProperties.getPropertyNamed(originalSenderModel, 'script');
                let scriptEditor = window.System.findScriptEditorByTargetId(originalSender.id);
                if(!scriptEditor){
                    this._openScriptEditor(originalSender.id);
                    scriptEditor = window.System.findScriptEditorByTargetId(originalSender.id);
                }
                if(scriptEditor){
                    target = scriptEditor.model;
                }
            }
            // TODO Sort this out
            if(target && text){
                let textLines = text.split("\n");
                // offending command text line with an error marker
                for(let i = 0; i < textLines.length; i++){
                    let line = textLines[i];
                    if(line.match(regex)){
                        textLines[i] = line += ` --<<<[MessageNotUnderstood: command; commandName: "${commandName}"]`;
                    }
                }
                text = textLines.join("\n");
                target.partProperties.setPropertyNamed(target, "text", text);

            }
            // finally open the debugger (or current version thereof)
            // NOTE: this is a bit dangerous, b/c if the System doesn't
            // handle the `openDebugger` command anywhere it will throw
            // a MNU error, which will then invoke this handler cuasing
            // an infinite loop!
            this._openDebugger(originalSender.id, offendingMessage.type, offendingMessage.commandName);
        }
    },

    _cleanDoItSCript(script){
        // clean up the DoIt script by removing the handler
        // newlines, tabs and spaces
        script = script.replace("on doIt", "");
        script = script.replace("end doIt", "");
        script = script.replace(/^[\n\t ]+/, "");
        script = script.replace(/[\n\t ]+$/, "");
        return script;
    },

    _openScriptEditor: function(partId){
        let target = window.System.partsById[partId];
        let msg = {
            type: "command",
            "commandName": "openScriptEditor",
            args: [partId]
        };
        target.sendMessage(msg, target);
    },

    _openGrammar: function(partId, ruleName){
        let target = window.System.partsById[partId];
        let msg = {
            type: "command",
            "commandName": "openGrammar",
            args: [partId, ruleName]
        };
        target.sendMessage(msg, target);
    },

    // At the moment this simply opens a st-window st-field with
    // information about the available commands for said parts
    _openDebugger: function(partId, type, name){
        let target = window.System.partsById[partId];
        let msg = {
            type: "command",
            "commandName": "openDebugger",
            args: [partId, type, name]
        };
        target.sendMessage(msg, target);
    }
};

export {
    errorHandler,
    errorHandler as default
};


<|start_filename|>js/objects/views/editors/Editor.js<|end_filename|>
import EditorTab from './EditorTab.js';
import EditorPropList from './EditorPropList.js';
import EditorMessenger from './EditorMessenger.js';
import EditorCustomList from './EditorCustomList.js';
import EditorSubpartsPane from './EditorSubpartsPane.js';
import partIcons from '../../utils/icons.js';
// PREAMBLE

// Add editor tab element
window.customElements.define('editor-tab', EditorTab);
window.customElements.define('editor-props-list', EditorPropList);
window.customElements.define('editor-custom-list', EditorCustomList);
window.customElements.define('editor-messenger', EditorMessenger);
window.customElements.define('editor-subparts', EditorSubpartsPane);

const closeButton = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#9e9e9e" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <line x1="18" y1="6" x2="6" y2="18" />
  <line x1="6" y1="6" x2="18" y2="18" />
</svg>
`;

const scriptIcon = `
<svg id='script' xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-code" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
    <path d="M14 3v4a1 1 0 0 0 1 1h4" />
    <path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" />
    <path d="M10 13l-1 2l1 2" />
    <path d="M14 13l1 2l-1 2" />
</svg>`;

const templateString = `
<style>
    :host {
        display: flex;
        flex-direction: column;
        position: absolute;
        transform: translateX(-105%);
        transition: transform 150ms linear;
        width: 400px;
        height: 100%;
        background-color: white;
        padding: 8px;
        border-right: 1px solid rgba(0, 0, 0, 0.5);
        box-shadow: 0px 1px 10px 2px rgba(0, 0, 0, 0.3);
    }
    
    :host(.open){
        transform: translateX(0%);
        transition: transform 150ms linear;
        z-index: 100;
    }

    :host(::after) {
        content: " ";
        height: 100%;
        width: 5px;
        background-color: black;
        display: block;
        position: absolute;
        top: 0;
        right: -10;
        box-shadow: 0px 0px 3px 10px rgba(100, 100, 100, 0.6);
    }

    ::slotted(editor-props-list:not(.show-pane)),
    ::slotted(editor-messenger:not(.show-pane)),
    ::slotted(editor-custom-list:not(.show-pane)),
    ::slotted(editor-subparts:not(.show-pane)){
        display: none;
    }

    #tab-area {
        display: inline-flex;
        align-items: center;
        justify-content: space-around;
        width: 100%;
    }
    
    #pane-area {
        display: block;
        flex: 1;
        margin-top: 20px;
        overflow: hidden;
    }

    #header-area {
        display: flex;
        font-family: 'Helvetica', sans-serif;
        margin-bottom: 20px;
    }

    .header-side {
        flex: 1;
        margin-top: 20px;
    }

    #display-area {
        display: flex;
        align-items: center;
        margin-bottom: 30px;
    }

    #header-area h3 {
        display: inline-block;
        margin: 0;
        margin-right: 8px;
        margin-left: 5px;
        font-size: 1.7rem;
    }

    #header-left > input {
        display: inline-block;
        padding: 4px;
        outline: none;
        border: 1px solid transparent;
        border-bottom: 1px solid rgba(100, 100, 100, 0.8);
        font-size: 1.1rem;
    }

    #header-right > button {
        width: 100%;
        background-color: transparent;
        border: 1px solid transparent;
        outline: none;
        display: flex;
        align-items: center;
        justify-content: flex-start;
        font-size: 0.85em;
    }

    #header-right > button:hover {
        cursor: pointer;
        border: 1px solid rgba(150, 150, 150, 0.3);
    }

    #header-right > button:active {
        border: 1px solid rgba(150, 150, 150, 0.8);
        background-color: rgba(220, 220, 220);
    }

    #header-right > button > svg {
        height: 1.3em;
        width: auto;
        margin-right: 8px;
    }

    #header-left {
        max-width: 80%;
    }

    #header-left span {
        font-family: monospace;
        font-size: 1.1rem;
        color: rgba(0, 0, 0, 0.5);
        text-overflow: ellipsis;
        overflow: hidden;
    }

    #icon-display-area {
        width: 1.7rem;
        height: 1.7rem;
        margin-bottom: 5px;
    }
    #icon-display-area > svg {
        width: 100%;
        height: 100%;
    }

    #close-button {
        display: block;
        position: absolute;
        top: 5;
        right: 5;
    }
    #close-button:hover {
        cursor: pointer;
    }
</style>
<div id="close-button">${closeButton}</div>
<div id="header-area">
    <div id="header-left" class="header-side">
        <div id="display-area">
            <div id="icon-display-area"></div>
            <h3></h3><span></span>
        </div>
        <input type="text" id="part-name-input"/>
    </div>
    <div id="header-right" class="header-side">
        <button id="edit-script-button">
            ${scriptIcon}
            <span>Edit Script</span>
        </button>
    </div>
</div>
<div id="tab-area">
    <editor-tab active="true" name="properties">Properties</editor-tab>
    <editor-tab name="custom">Custom</editor-tab>
    <editor-tab name="messenger">Messenger</editor-tab>
    <editor-tab name="subparts">Subparts</editor-tab>
</div>
<div id="pane-area">
    <slot></slot>
</div>
`;

class Editor extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Bound methods
        this.toggle = this.toggle.bind(this);
        this.open = this.open.bind(this);
        this.close = this.close.bind(this);
        this.render = this.render.bind(this);
        this.centerOnElement = this.centerOnElement.bind(this);
        this.undoCenterOnElement = this.undoCenterOnElement.bind(this);
        this.updateHeader = this.updateHeader.bind(this);
        this.checkForNavigation = this.checkForNavigation.bind(this);
        this.receiveMessage = this.receiveMessage.bind(this);
        this.onTabActivated = this.onTabActivated.bind(this);
        this.onNameInputChange = this.onNameInputChange.bind(this);
        this.onEditScriptClick = this.onEditScriptClick.bind(this);
        this.onCloseButtonClick = this.onCloseButtonClick.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this._shadowRoot.addEventListener('tab-activated', this.onTabActivated);
            this._shadowRoot.getElementById('close-button').addEventListener(
                'click',
                this.onCloseButtonClick
            );

            // Events
            let nameInput = this._shadowRoot.getElementById('part-name-input');
            nameInput.addEventListener('change', this.onNameInputChange);

            let editScriptButton = this._shadowRoot.getElementById('edit-script-button');
            editScriptButton.addEventListener('click', this.onEditScriptClick);
        }
    }

    disconnectedCallback(){
        this._shadowRoot.removeEventListener('tab-activated', this.onTabActivated);
        this._shadowRoot.getElementById('close-button').removeEventListener(
            'click',
            this.close
        );

        // Events
        let nameInput = this._shadowRoot.getElementById('part-name-input');
        nameInput.removeEventListener('change', this.onNameInputChange);

        let editScriptButton = this._shadowRoot.getElementById('edit-script-button');
        editScriptButton.removeEventListener('click', this.onEditScriptClick);
    }

    toggle(){
        if(this.isOpen){
            this.close();
        } else {
            this.open();
        }
    }

    open(){
        this.classList.add('open');
        this.centerOnElement();
    }

    close(){
        this.classList.remove('open');
        this.undoCenterOnElement();
    }

    render(aModel){
        if(this.model){
            this.model.removePropertySubscriber(this);
        }
        this.model = aModel;
        this.model.addPropertySubscriber(this);

        // If the incoming model is a Card or
        // Stack that is not the current one,
        // we navigate to it
        if(this.model.type == 'card' || this.model.type == 'stack'){
            this.checkForNavigation();
        }

        this.updateHeader();

        // Clear slotted inner DOM
        this.innerHTML = "";

        // Add panes
        let propsPane = document.createElement('editor-props-list');
        propsPane.setAttribute('tab-name', 'properties');
        this.appendChild(propsPane);
        propsPane.render(this.model);

        let messengerPane = document.createElement('editor-messenger');
        messengerPane.setAttribute('tab-name', 'messenger');
        this.appendChild(messengerPane);
        messengerPane.render(this.model);

        let customPane = document.createElement('editor-custom-list');
        customPane.setAttribute('tab-name', 'custom');
        this.appendChild(customPane);
        customPane.render(this.model);

        let subpartsPane = document.createElement('editor-subparts');
        subpartsPane.setAttribute('tab-name', 'subparts');
        this.appendChild(subpartsPane);
        subpartsPane.render(this.model);

        // Find the active tab and show its corresponding pane
        let activeTab = this._shadowRoot.querySelector(`editor-tab[active="true"]`);
        if(activeTab){
            let activeName = activeTab.getAttribute('name');
            Array.from(this.querySelectorAll('[tab-name]')).forEach(pane => {
                let name = pane.getAttribute('tab-name');
                if(name == activeName){
                    pane.classList.add('show-pane');
                } else {
                    pane.classList.remove('show-pane');
                }
            });
        }

        // If this pane is already open, then center
        // on the primary view element for the model
        if(this.isOpen){
            this.centerOnElement();
        }
    }

    centerOnElement(){
        // Use CSS transforms of the whole World to center on
        // the primary view element of the Part being edited,
        // if set. If not set, do nothing.
        if(this.model){
            // If we are editing a Card, Stack, or World, then
            // we uncenter and return
            let isCardStackOrWorld = ['card', 'stack', 'world'].includes(this.model.type);
            if(isCardStackOrWorld){
                return this.undoCenterOnElement();
            }
            
            let partView = window.System.findViewById(this.model.id);
            let worldView = window.System.findViewById('world');
            let current = worldView.getAttribute('centered-on');
            if(current == this.model.id.toString()){
                return;
            }

            let menuRect = this.getBoundingClientRect();
            let partRect = partView.getBoundingClientRect();

            // Get the actual viewable width, plus the editor menu
            let viewWidth = window.innerWidth + menuRect.width;
            let viewHeight = window.innerWidth - menuRect.height;

            // Calculate X translation
            let targetX = (viewWidth - partRect.width) / 2;
            let newX;
            if(targetX < partRect.left){
                newX = (partRect.left - targetX) * -1;
            } else {
                newX = targetX - partRect.left;
            }

            // Calculate Y translation
            let targetY = (viewHeight - partRect.height) / 2;
            let newY;
            if(targetY < partRect.top){
                newY = (partRect.top - targetY) * -1;
            } else {
                newY = targetY - partRect.top;
            }

            worldView.setAttribute('centered-on', this.model.id);
            
            // Set transform and transition
            worldView.style.transition = "transform 0.3s ease-out";
            worldView.style.transform = `translate(${newX}px, ${newY}px)`;
        }
    }

    undoCenterOnElement(){
        let worldView = window.System.findViewById('world');
        worldView.removeAttribute('centered-on');
        worldView.style.removeProperty('transform');
    }

    updateHeader(){
        let nameInput = this._shadowRoot.querySelector('#header-left > input');
        let typeDisplay = this._shadowRoot.querySelector('#display-area > h3');
        let idDisplay = this._shadowRoot.querySelector('#display-area > span');
        let iconDisplay = this._shadowRoot.getElementById('icon-display-area');

        let partName = this.model.partProperties.getPropertyNamed(
            this.model,
            'name'
        );

        if(partName && partName !== ""){
            nameInput.value = partName;
        } else {
            nameInput.value = "(Unnamed)";
        }

        typeDisplay.textContent = this.model.type.charAt(0).toUpperCase() + this.model.type.slice(1);
        idDisplay.textContent = `id=${this.model.id}`;
        idDisplay.title = this.model.id;

        if(Object.keys(partIcons).includes(this.model.type)){
            iconDisplay.innerHTML = partIcons[this.model.type];
        } else {
            iconDisplay.innerHTML = partIcons.generic;
        }
    }

    checkForNavigation(){
        // If the model is a Card or Stack that
        // is not the current (ie, not being displayed
        // in the main window), then we should navigate
        // to it
        let currentStack = window.System.world.currentStack;
        let currentCard = currentStack.currentCard;
        if(this.model.type == 'card' && this.model.id != currentCard.id){
            currentStack.goToCardById(this.model.id);
        } else if(this.model.type == 'stack' && this.model.id != currentStack.id){
            window.System.world.goToStackById(this.model.id);
        }
        
    }

    receiveMessage(aMessage){
        switch(aMessage.type){
        case 'propertyChanged':
            // Find any nested editor-prop-item elements
            // and re-render, so they display the correct
            // values in the editor
            let queryString = `editor-prop-item[name="${aMessage.propertyName}"][owner-id="${aMessage.partId}"]`;
            Array.from(this.querySelectorAll(queryString)).forEach(el => {
                if(el.property.value !== aMessage.value){
                    el.render();
                }
            });
            break;
        }
    }

    onTabActivated(event){
        Array.from(this._shadowRoot.querySelectorAll('editor-tab'))
            .filter(tabEl => {
                return tabEl !== event.target;
            }).forEach(tabEl => {
                tabEl.removeAttribute('active');
            });

        // Get the name of the activated tab
        let targetName = event.target.getAttribute('name');
        Array.from(this.querySelectorAll('[tab-name]')).forEach(pane => {
            let name = pane.getAttribute('tab-name');
            if(name == targetName){
                pane.classList.add('show-pane');
            } else {
                pane.classList.remove('show-pane');
            }
        });
    }

    onNameInputChange(event){
        if(this.model){
            let newName = event.target.value;
            this.model.partProperties.setPropertyNamed(
                this.model,
                'name',
                newName
            );
        }
    }

    onEditScriptClick(event){
        if(this.model){
            this.model.sendMessage({
                type: 'command',
                commandName: 'closeEditor',
                args: [
                    this.model.id
                ]
            }, this.model);
            this.model.sendMessage({
                type: 'command',
                commandName: 'openScriptEditor',
                args: [
                    this.model.id
                ]
            }, this.model);
        }
    }

    onCloseButtonClick(event){
        // it's possible that no model has been set
        if(this.model){
            this.model.sendMessage({
                type: 'command',
                commandName: 'closeEditor',
                args: [
                    this.model.id
                ]
            }, this.model);
        } else {
            this.close();
        }
    }

    get isOpen(){
        return this.classList.contains('open');
    }
};

export {
    Editor,
    Editor as default
};


<|start_filename|>js/objects/views/BrowserView.js<|end_filename|>
import PartView from './PartView.js';

const linkIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5" />
  <path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5" />
</svg>
`;

const templateString = `
<style>
:host {
    box-sizing: border-box;
    display: block;
    position: absolute;
    padding: 1px;
    user-select: none;
    cursor: pointer;
}
iframe {
    width: 100%;
    height: 100%;
}
</style>
<iframe frameborder="0" allowfullscreen></iframe>
`;

class BrowserView extends PartView {
    constructor(){
        super();

        // Setup template and shadow dom
        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(this.template.content.cloneNode(true));

        // Bind component methods
        this.onClick = this.onClick.bind(this);
        this.initCustomHaloButton = this.initCustomHaloButton.bind(this);
        this.updateBrowserLink = this.updateBrowserLink.bind(this);
    }

    afterConnected(){
        if(!this.haloButton){
            this.initCustomHaloButton();
        }
    }

    afterDisconnected(){
    }

    afterModelSet(){
        let iframe = this._shadowRoot.querySelector("iframe");
        let src = this.model.partProperties.getPropertyNamed(this.model, "src");
        if(src){
            iframe.src = src;
        }
        this.onPropChange("src", (url) => {
            try{
                // resource load is auto-loaded by the <browser> element
                iframe.src = url;
            } catch(error){
                let errorMsg = {
                    type: "error",
                    name: "ResourceNotFound",
                    resourceType: "browser",
                    partId: this.model.id,
                    details: {source: url, type: "url"}

                };
                this.model.sendMessage({errorMsg}, this.model);
            }
        });
    }

    onClick(event){
        if(event.button == 0){
            if(event.shiftKey){
                // prevent triggering the on click message
                event.preventDefault();
                if(this.hasOpenHalo){
                    this.model.partProperties.setPropertyNamed(this.model, "halo-open", false);
                } else {
                    this.model.partProperties.setPropertyNamed(this.model, "halo-open", true);
                }
            } else if(!this.hasOpenHalo){
                // Send the click command message to self
                this.model.sendMessage({
                    type: 'command',
                    commandName: 'click',
                    args: [],
                    shouldIgnore: true // Should ignore if System DNU
                }, this.model);
            }
        }
    }

    openHalo(){
        // Override default. Here we add a custom button
        // when showing.
        let foundHalo = this.shadowRoot.querySelector('st-halo');
        if(!foundHalo){
            foundHalo = document.createElement('st-halo');
            this.shadowRoot.appendChild(foundHalo);
        }
        foundHalo.append(this.haloButton);
    }

    initCustomHaloButton(){
        this.haloButton = document.createElement('div');
        this.haloButton.id = 'halo-browser-link';
        this.haloButton.classList.add('halo-button');
        this.haloButton.innerHTML = linkIcon;
        this.haloButton.style.marginTop = "6px";
        this.haloButton.setAttribute('slot', 'right-column');
        this.haloButton.setAttribute('title', 'Edit link for browser source');
        this.haloButton.addEventListener('click', this.updateBrowserLink);
    }

    updateBrowserLink(event){
        // Tells the model to update its
        // src link for the browser
        let currentSrc = this.model.partProperties.getPropertyNamed(
            this.model,
            'src'
        );
        let result = window.prompt("Edit URL for browser:", currentSrc);
        if(result && result !== '' && result !== currentSrc){
            this.sendMessage(
                {
                    type: 'command',
                    commandName: 'setURLTo',
                    args: [ result ]
                },
                this.model
            );
        }
    }


};

export {
    BrowserView,
    BrowserView as default
};


<|start_filename|>js/ohm/tests/test-string-grammar.js<|end_filename|>
/**
 * String and stringLiteral
 * related grammatical tests
 **/
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
}
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};

const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
};

describe("Basic stringLiteral", () => {
    it('Can deal with a single word', () => {
        const s = '"test"';
        semanticMatchTest(s, 'stringLiteral');
        semanticMatchTest(s, 'Factor');
    });
    it('Can deal with a sentence', () => {
        const s = '"this is a test"';
        semanticMatchTest(s, 'stringLiteral');
        semanticMatchTest(s, 'Factor');
    });
    it('Can deal with a url', () => {
        const s = '"http://localhost:8000/js/objects/examples/bootstrap-example.html"';
        semanticMatchTest(s, 'stringLiteral');
        semanticMatchTest(s, 'Factor');
    });
    it('Can deal with a path', () => {
        const s = '"../../examples/bootstrap-example.html"';
        semanticMatchTest(s, 'stringLiteral');
        semanticMatchTest(s, 'Factor');
    });
    it('Can deal with whitespace', () => {
        const s = '" \t   hi \t  \s  "';
        semanticMatchTest(s, 'stringLiteral');
        semanticMatchTest(s, 'Factor');
    });
    it('Can deal with characters that look like operators', () => {
        const s = `"hello ** this / is ^ a stringLiteral +"`;
        semanticMatchTest(s, 'stringLiteral');
        semanticMatchTest(s, 'Factor');
    });
    it('Does not match if newline is present', () => {
        const s = '"this is a\t\ntest"';
        semanticMatchFailTest(s, 'stringLiteral');
        semanticMatchFailTest(s, 'Factor');
    });
});

describe("Concatenation Tests", () => {
    it("Can concat two stringLiterals", () => {
        let s = `"some first string" & "some second string"`;
        semanticMatchTest(s, "Expression");
        semanticMatchTest(s, "Expression_stringConcatExpr");
    });
    it("Can concat two stringLiterals containing & symbol", () => {
        let s = `"& this is the string" & "this is another && string"`;
        semanticMatchTest(s, "Expression");
        semanticMatchTest(s, "Expression_stringConcatExpr");
    });
    it("Can concat a stringLiteral and a variableName", () => {
        let s = `"This is the first part" & someStringVar`;
        semanticMatchTest(s, "Expression");
        semanticMatchTest(s, "Expression_stringConcatExpr");
    });
    it("Can concat a variableName and a stringLiteral", () => {
        let s = `someStringVar & "This is a string"`;
        semanticMatchTest(s, "Expression");
        semanticMatchTest(s, "Expression_stringConcatExpr");
    });
    it("Can concat two variableNames", () => {
        let s = `firstStringVar & secondStringVar`;
        semanticMatchTest(s, "Expression");
        semanticMatchTest(s, "Expression_stringConcatExpr");
    });
    it("Can concat several relevant rules at once", () => {
        let s = `myStringVar & "This is a literal string" & anotherVar & "Some string with & symbol"`;
        semanticMatchTest(s, "Expression");
        semanticMatchTest(s, "Expression_stringConcatExpr");
    });
});


<|start_filename|>plugins/plugins.js<|end_filename|>
import Test from './testAPI.js';
import Merriam from './merriam.js';

const plugins = {
    "MerriamAPI": Merriam,
    "TestAPI": Test
}

export {
    plugins,
    plugins as default
}


<|start_filename|>developer/devtools/inspector.js<|end_filename|>
/*
 * I am run whenever the devtools are open and am responsible
 * for creating panels and setting up communiction (ports)
 * between them and the background script.
 *
/**
Create a panel, and add listeners for panel show/hide events.
*/
browser.devtools.panels.create(
    "Message Inspector Panel",
    "/icons/distill-48.jpg",
    "/panels/panel.html"
).then((panel) => {
    var _window; // hold a reference to panel.html

    // if the panel window is undefined, i.e. the panel is closed
    // we need to store the data coming through the port
    var data = [];
    // create a connection/port which will handle all communication
    // between the panel and the background script
    var portFromPanel = browser.runtime.connect({name: "port-from-panel"});
    portFromPanel.onMessage.addListener(function(msg) {
        if (_window){
            // handleMessageFromBackground() is defined in panel.js
            _window.handleMessageFromBackground(msg);
        } else {
            // if the panel's window is undefined store the data for now
            data.push(msg);
        }
    });

    // when the panel button is clicked
    panel.onShown.addListener(function tmp(panelWindow) {
        console.log("panel is being shown");
        // clean up any stale listeners
        panel.onShown.removeListener(tmp);

        // set the _window var to panelWindow which allows handling
        // of messages by the panel, i.e. in the panel's window context
        _window = panelWindow;
        var msg;
        // if any data was logged while the panel was not available
        // send it along now
        while (msg = data.shift()){
            _window.handleMessageFromBackground(msg);
        };
        // If we ever need to send messages back via the port
        // we can do that as below
        _window.respond = function(msg) {
             portFromPanel.postMessage(msg);
        }
    });

    panel.onHidden.addListener(function() {console.log("panel is being hidden")});
});


<|start_filename|>js/objects/tests/test-views-lifecycle-with-preload.js<|end_filename|>
/**
 * View-specific lifecycle Tests
 * --------------------------------------
 * These are integration tests that ensure
 * proper lifecycle for specifc views and models.
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

describe('Button Views', () => {
    it('Can setup world with a stack, card and button', () => {
        let currentStackViews = document.querySelectorAll('st-world st-stack.current-stack');
        assert.equal(currentStackViews.length, 1);
        let currentStackModel = currentStackViews[0].model;
        let cardViews = document.querySelectorAll('st-world st-stack.current-stack > st-card');
        assert.equal(cardViews.length, 1);
        let currentCardModel = cardViews[0].model;
        // add a named button
        let newButtonMsg = {
            type: 'command',
            commandName: 'newModel',
            args: ['button', currentCardModel.id,"New Button"]
        };
        System.receiveMessage(newButtonMsg);
        let buttonViews = cardViews[0].querySelectorAll('st-button');
        assert.equal(buttonViews.length, 1);
    });
    it('Button has the proper name (ButtonView..afterConnected() was called)', () => {
        let cardView = document.querySelector('st-world st-stack.current-stack > st-card');
        let buttonView = cardView.querySelector('st-button');
        assert.equal("New Button", buttonView.innerText);
    });
});


<|start_filename|>js/objects/tests/test-system-with-preload.js<|end_filename|>
/**
 * System Tests
 * --------------------------------------
 * These are integration tests that ensure
 * proper functioning of the System and its
 * various methods, functions and utilities.
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

describe('System methods', () => {
    it('Can setup world with a multi-stack multi-card stack', () => {
        let currentStackViews = document.querySelectorAll('st-world st-stack.current-stack');
        assert.equal(currentStackViews.length, 1);
        let currentStackModel = currentStackViews[0].model;
        let cardViews = document.querySelectorAll('st-world st-stack.current-stack > st-card');
        assert.equal(cardViews.length, 1);
        // add a few more cards
        let newCardMsg = {
            type: 'command',
            commandName: 'newModel',
            args: ['card', currentStackModel.id]
        };
        System.receiveMessage(newCardMsg);
        System.receiveMessage(newCardMsg);
        cardViews = document.querySelectorAll('st-world st-stack.current-stack > st-card');
        assert.equal(cardViews.length, 3);
        let currentCardViews = document.querySelectorAll('st-world st-stack.current-stack > st-card.current-card');
        assert.equal(currentCardViews.length, 1);
        // add a few more stacks
        let worldView = document.querySelector('st-world');
        let newStackMsg = {
            type: 'command',
            commandName: 'newModel',
            args: ['stack', worldView.model.id]
        };
        System.receiveMessage(newStackMsg);
        System.receiveMessage(newStackMsg);
        let stackViews = document.querySelectorAll('st-world > st-stack');
        assert.equal(stackViews.length, 3);
    });
    it('Can get current-card model.', () => {
        let currentCardView = document.querySelector('st-stack.current-stack > st-card.current-card');
        let currentCardModel = currentCardView.model;
        assert.equal(currentCardModel.id, System.getCurrentCardModel().id);
    });

    // There are not multiple at the time this test runs!
    it.skip('Will throw error if there are multiple .current-stack > .current-card elements', () => {
        let cardViews = document.querySelectorAll('st-stack.current-stack > st-card');
        let card2 = cardViews[0];
        card2.classList.add("current-card");
        let getCurrentCardModel = function() {System.getCurrentCardModel();};
        expect(getCurrentCardModel).to.throw();
        card2.classList.remove("current-card");
    });
    it('Can get current-stack model.', () => {
        let currentStackView = document.querySelector('st-stack.current-stack');
        let currentStackModel = currentStackView.model;
        assert.equal(currentStackModel.id, System.getCurrentStackModel().id);
    });
    // There are not multiple at the time this test runs!
    it.skip('Will throw error if there are multiple st-world > .current-stack elements', () => {
        let stackViews = document.querySelectorAll('st-world > st-stack');
        let stack2 = stackViews[0];
        stack2.classList.add("current-stack");
        let getCurrentStackModel = function() {System.getCurrentStackModel();};
        expect(getCurrentStackModel).to.throw();
        stack2.classList.remove("current-stack");
    });
    describe('System Interrupt', () => {
        let currentCard;
        let buttons;
        before('', () => {
            currentCard = System.getCurrentCardModel();
            for(let i =0; i < 2; i++){
                let msg = {
                    type: 'command',
                    commandName: 'newModel',
                    args: ['button', currentCard.id]
                };
                currentCard.sendMessage(msg, currentCard);
            }
            buttons = document.querySelectorAll('st-card.current-card > st-button');
            // put a 'step' handler so it doesn't MNU when 'stepping' is set to true
            let script = [
                'on step',
                '    answer "OK"',
                'end step'
            ].join('\n');
            buttons.forEach((button) => {
                let msg = {
                    type: 'compile',
                    targetId: button.model.id,
                    codeString: script
                };
                button.model.sendMessage(msg, button.model);
                button.model.partProperties.setPropertyNamed(button.model, 'stepping', true);
            });
        });
        it('Global Interrupt message stops all stepping .', () => {
            currentCard.sendMessage({
                type: "command",
                commandName: "globalInterrupt",
                args: []
            }, currentCard);
            buttons.forEach((button) => {
                assert.isFalse(button.model.partProperties.getPropertyNamed(button.model, 'stepping'));
            });
        });
        it.skip('Global Interrupt key-combo (ctrl-c) stops all stepping .', () => {
            buttons.forEach((button) => {
                button.model.partProperties.setPropertyNamed(button.model, 'stepping', true);
            });
            let event = new window.Event('keydown');
            event.ctrlKey = true;
            event.key = 'c';
            document.dispatchEvent(event);
            buttons.forEach((button) => {
                assert.isFalse(button.model.partProperties.getPropertyNamed(button.model, 'stepping'));
            });
        });
    });
});


<|start_filename|>js/objects/views/editors/EditorSubpartsPane.js<|end_filename|>
// PREAMBLE
import EditorLocationInfo from './EditorLocationInfo.js';
import partIcons from '../../utils/icons.js';
import {onLocationLinkClick, getLocationStringFor} from './utils/subparts.js';

window.customElements.define('editor-location-info', EditorLocationInfo);

const clipboardIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" />
  <rect x="9" y="3" width="6" height="4" rx="2" />
</svg>
`;

const templateString = `
<style>
    :host {
        display: flex;
        flex-direction: column;
        width: 100%;
        height: 100%;
        font-family: 'Helvetica', sans-serif;
        font-size: 0.8rem;
    }

    :host() > li {
        background-color: red;
    }

    .id-link,
    .location-link {
        display: inline-flex;
        align-items: center;
        outline: none;
        border: none;
        border-bottom: 1px solid rgba(150, 150, 150, 0.3);
        transition: border 0.2s ease-out;
        vertical-alignment: center;
        background-color: transparent;
        padding: 0px;
        font-size: 1em;
    }

    .id-link:hover,
    .location-link:hover {
        cursor: pointer;
        border-bottom: 1px solid rgba(150, 150, 150, 0.7);
        transition: border 0.2s ease-out;
    }

    .id-link > svg,
    .location-link > svg {
        margin-left: 8px;
        opacity: 0.7;
        transform: translateX(0px);
        transition: transform 0.2s ease-out, opacity 0.2s ease-out;
    }

    .id-link:hover > svg,
    .location-link:hover > svg {
        opacity: 1.0;
        transform: translateX(-5px);
        transition: transform 0.2s ease-out, opacity 0.2s ease-out;
    }

    section {
        display: flex;
        flex-direction: column;
        margin: 6px;
    }

    #button-area {
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        justify-content: space-between;'
    }

    #subparts-list-wrapper {
        flex: 1;
        overflow-y: hidden;
    }

    .hidden {
        display: none;
    }

    #subparts-area {
        flex: 1;
        list-style: none;
        font-family: 'Helvetica', sans-serif;
        padding: 0;
        margin: 0;
        margin-left: 32px;
        overflow-y: auto;
    }
</style>
<section id="button-area">
    <slot name="button"></slot>
</section>
<section id="location-area">
    <h3>Part Location and Owners</h3>
    <p class="part-info">
        I am located at <button class="location-link"><span></span>${clipboardIcon}</button>
        and my id is <button class="id-link"><span></span>${clipboardIcon}</button>
    </p>
    <slot name="ancestor-info"></slot>
</section>
<section id="subparts-list-wrapper">
    <h3>Subparts</h3>
    <ol id="subparts-area">
        <slot></slot>
    </ol>
</section>
`;

class EditorSubpartsPane extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // define and bind methods
        this.getLocationStringFor = getLocationStringFor.bind(this);
        this.onLocationLinkClick = onLocationLinkClick.bind(this);

        // Bound methods
        this.onAddSubpart = this.onAddSubpart.bind(this);
        this.onSubpartItemClick = this.onSubpartItemClick.bind(this);
        this.onSubpartItemMouseEnter = this.onSubpartItemMouseEnter.bind(this);
        this.onSubpartItemMouseLeave = this.onSubpartItemMouseLeave.bind(this);
        this.createAddPartButton = this.createAddPartButton.bind(this);
        this.createSubpartComponent = this.createSubpartComponent.bind(this);
        this.getLocationViews = this.getLocationViews.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.headerEl = this._shadowRoot.getElementById('location-area');
            this.myLocationArea = this.headerEl.querySelector('p');
            this.myLocationButton = this.myLocationArea.querySelector('.location-link');
            this.myIdButton = this.myLocationArea.querySelector('.id-link');

            // Add event listener to buttons
            this.myLocationButton.addEventListener('click', this.onLocationLinkClick);
            this.myIdButton.addEventListener('click', this.onLocationLinkClick);
        }
    }

    disconnectedCallback(){
        this.myLocationButton.removeEventListener('click', this.onLocationLinkClick);
        this.myIdButton.removeEventListener('click', this.onLocationLinkClick);
    }

    render(aModel){
        this.model = aModel;
        this.headerEl = this._shadowRoot.getElementById('location-area');

        // Clear any DOM children
        this.innerHTML = "";


        // Create location link elements
        // and also the self-location element
        let myLocationText = this.getLocationStringFor(this.model);
        this.myLocationButton.querySelector('span').textContent = myLocationText;
        this.myIdButton.querySelector('span').textContent = this.model.id.toString();
        if(this.model.type == 'world'){
            this.headerEl.classList.add('hidden');
        } else {
            this.headerEl.classList.remove('hidden');

            // Create the info elements
            ['stack', 'card', 'owner'].forEach(kind => {
                let infoEl = document.createElement('editor-location-info');
                infoEl.setAttribute('slot', 'ancestor-info');
                infoEl.setAttribute('kind', kind);
                infoEl.render(this.model);
                this.appendChild(infoEl);
            });
        }

        // Create the "add subpart" buttons for parts that are accepted by the
        // current Model part.
        this.model.acceptedSubpartTypes.forEach(partType => {
            let element = this.createAddPartButton(partType);
            this.appendChild(element);
        });

        let labelHeader = this._shadowRoot.querySelector('#subparts-list-wrapper > h3');
        if(this.model.subparts.length){
            labelHeader.textContent = "Current Subparts";
        } else {
            labelHeader.textContent = "There are no subparts";
        }

        this.model.subparts.forEach(subpart => {
            let element = this.createSubpartComponent(subpart);
            this.appendChild(element);
        });
    }

    createSubpartComponent(aPart){
        let wrapper = document.createElement('li');
        wrapper.classList.add('subpart-item');
        wrapper.setAttribute('ref-id', aPart.id);
        wrapper.addEventListener('click', this.onSubpartItemClick);
        wrapper.addEventListener('mouseenter', this.onSubpartItemMouseEnter);
        wrapper.addEventListener('mouseleave', this.onSubpartItemMouseLeave);

        // Add icon area an SVG for Part
        let iconArea = document.createElement('div');
        iconArea.classList.add('icon-display-area');
        let iconImage;
        if(Object.keys(partIcons).includes(aPart.type)){
            iconImage = partIcons[aPart.type];
        } else {
            iconImage = partIcons.generic;
        }
        iconArea.innerHTML = iconImage;
        wrapper.append(iconArea);

        // Add label, name, and id info
        let labelArea = document.createElement('h3');
        labelArea.textContent = `a ${aPart.type[0].toUpperCase()}${aPart.type.slice(1)}`;
        wrapper.append(labelArea);
        let name = aPart.partProperties.getPropertyNamed(
            aPart,
            'name'
        );
        if(name && name != ""){
            let nameArea = document.createElement('span');
            nameArea.classList.add('name-span');
            nameArea.textContent = `"${name}"`;
            wrapper.append(nameArea);
        }

        let idArea = document.createElement('span');
        idArea.classList.add('id-span');
        idArea.textContent = `(${aPart.id})`;
        wrapper.append(idArea);

        return wrapper;
    }

    createAddPartButton(aPartName){
        let button = document.createElement('button');
        let icon = partIcons[aPartName];
        if(!icon){
            icon = partIcons.generic;
        }
        button.setAttribute('slot', 'button');
        button.setAttribute('data-type', aPartName);
        button.setAttribute('title', `Add a ${aPartName} to this ${this.model.type}`);
        button.classList.add('add-part-button');
        button.addEventListener('click', this.onAddSubpart);
        button.innerHTML = icon;
        return button;
    }

    onSubpartItemClick(event){
        let id = event.currentTarget.getAttribute('ref-id');
        let targetPart = window.System.partsById[id];
        if(targetPart){
            window.System.editor.render(targetPart);
        }
    }

    onSubpartItemMouseEnter(event){
        this.getLocationViews(event).forEach((view) => {
            view.highlight("rgb(54, 172, 100)"); // green
        });
    }

    onSubpartItemMouseLeave(event){
        this.getLocationViews(event).forEach((view) => {
            view.unhighlight();
        });
    }

    onAddSubpart(event){
        let type = event.currentTarget.getAttribute('data-type');
        if(type){
            this.model.sendMessage({
                type: 'command',
                commandName: 'newModel',
                args: [
                    type,
                    this.model.id
                ]
            }, this.model);
        }
        this.render(this.model);
    }

    getLocationViews(event){
        let targetId = event.currentTarget.getAttribute('ref-id');
        let span = event.currentTarget.querySelector('span');
        return window.System.findViewsById(targetId);
    }
};

export {
    EditorSubpartsPane,
    EditorSubpartsPane as default
};


<|start_filename|>developer/devtools/panels/panel.js<|end_filename|>
/*
 * I am the inspector panel.
 * I handle incoming messages from background and
 * send sometimes inject scripts directly into the target
 * window.
 *
 * I also handle how the information is displayed.
 */

var clearButton = document.getElementById("clear-button");
var tablulatordata = [];

var idClick = function(e, cell) {
    console.log("cell clicked");
    var celltext = cell.getValue()
    var id = celltext.match(/\(id=(.*)\)/)[1]
    if (id) {
        var inspectCommand = `inspect(document.querySelector('[part-id="${id}"]'))`;
        browser.devtools.inspectedWindow.eval(inspectCommand).then(handleResult);
    }
}

var table = new Tabulator("#tabulator-table", {
    // height:600, having a set height of table enables the Virtual DOM and improves render speed dramatically (can be any valid css height value)
    // if performance becomes a problem, go back to this solution
    maxHeight:"100%",
    data:tablulatordata, //assign data to table
    reactiveData:true,
    layout:"fitColumns", //fit columns to width of table (optional)
    columns:[
        {title:"Time", field:"time", width:150},
        {title:"Message", field:"message", headerFilter:true, formatter:"textarea"},
        {title:"Original Sender", field:"originalSender", headerFilter:true, formatter:"textarea", cellClick: idClick},
        {title:"Sender", field:"sender", headerFilter:true, formatter:"textarea", cellClick: idClick},
        {title:"Receiver", field:"receiver", headerFilter:true, formatter:"textarea", cellClick: idClick} ,
        {title:"Tree", field:"tree", formatter:"textarea"} ,
    ],
    // by default, this library doesn't apply existing filters to new rows, so here we manually reset
    // those column filters when a new row shows up
    rowAdded:function(row){
        filters = table.getHeaderFilters();
        table.clearFilter(true);
        filters.forEach(function (filter) {
            table.setHeaderFilterValue(filter.field, filter.value);
        });
    },
});

clearButton.onclick = function() {
    console.log("clearing data")
    tablulatordata = [];
    table.setData(tablulatordata);
}

// recursively produce a stringified version of
// the tree, using a tabbed display of underscores
// to represent the node-to-child relationship
function tabularizeTree(node, tabCount) {
    var s = "__".repeat(tabCount);
    s += node.type + `(id=${node.id})` + "\n";
    node.children.forEach(child => {
        s += tabularizeTree(child, tabCount + 1)
    })
    return s;
}

function formatNode(node) {
   return `${node.name} (id=${node.id})`
}

function handleMessageFromBackground(msg) {
    console.log("getting message from background");
    if (!msg.msg || !msg.source || !msg.target || !msg.tree) {
        console.log(`invalid message: ${msg}`)
        return;
    }

    let now = new Date();
    var j = {
        time: now.toLocaleTimeString([], {hour12: false}) + `.${now.getMilliseconds()}`,
        message: JSON.stringify(msg.msg, null, '\t'),
        sender: formatNode(msg.source),
        receiver: formatNode(msg.target),
        tree: tabularizeTree(msg.tree, 0),
        originalSender: formatNode(msg.msg.senders[0])
    }
    tablulatordata.push(j)
}

function _prepMessage(msg){
    let td = document.createElement("td");
    let pre = document.createElement("pre");
    pre.textContent = JSON.stringify(msg, null, '\t');
    td.appendChild(pre);
    return td;
}

function handleResult(result){
    if(result[1]){
        console.error(result);
    }
}


<|start_filename|>js/objects/tests/test-system-toolbox-with-preload.js<|end_filename|>
/**
 * System Toolbox Window Tests
 * --------------------------------------
 * These are integration tests that ensure
 * we are creating, properly opening, and
 * have the correct functionality in the Toolbox
 * when we send the message to the System that
 * it should be opened.
 * To open a Toolbox window, we send the
 * 'openToolbox' command message to System.
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

/** IMPORTANT: Toolbox is now a serialized part, so
    we are skipping these tests for now and might
    remove altogether
**/


describe.skip('Opening a Toolbox', () => {
    it('There is exactly one open window (toolbox) in the world', () => {
        let foundWindows = document.querySelectorAll('st-window');
        assert.equal(foundWindows.length, 1 );
    });
    it('Can remove the current window from the world for further testing (TODO! make programmatic) ', () => {
        let foundWindows = document.querySelectorAll('st-window');
        foundWindows.forEach((node) => {node.remove()});
        foundWindows = document.querySelectorAll('st-window');
        assert.equal(foundWindows.length, 0);
    });
    it('Sending the #openToolbox message to System will be understood', () => {
        let openMsg = {
            type: 'command',
            commandName: 'openToolbox',
            args: [] // No id given, System assumes current stack
        };
        let sendFunction = function(){
            System.receiveMessage(openMsg);
        };
        expect(sendFunction).to.not.throw(Error);
    });
    it('Current StackView now has WindowView as a child element', () => {
        let currStackView = document.querySelector('st-stack.current-stack');
        assert.exists(currStackView);
        let windowView = document.querySelector('st-window');
        assert.exists(windowView);

        let foundWindow = currStackView.querySelector('st-window');
        assert.equal(foundWindow, windowView);
    });
    it('Current Stack Part now has a Window as a subpart', () => {
        let currentStack = document.querySelector('st-stack.current-stack').model;
        assert.exists(currentStack);
        let windowModel = document.querySelector('st-window').model;
        assert.exists(windowModel);

        assert.include(currentStack.subparts, windowModel);
    });
});

describe.skip('Toolbox functionality', () => {
    /* Note: For now we only have
       the add button to card functionality
    */
    let currentCardView;
    let currentStackView;
    let windowStackView;
    let windowCurrentCardView;
    before(() => {
        currentStackView = document.querySelector('st-stack.current-stack');
        currentCardView = currentStackView.querySelector('.current-card');
        windowStackView = currentStackView.querySelector('st-window st-stack');
        windowCurrentCardView = windowStackView.querySelector('.current-card');
    });
    it('Has all appropriate views', () => {
        assert.exists(currentStackView);
        assert.exists(currentCardView);
        assert.exists(windowStackView);
        assert.exists(windowCurrentCardView);
    });
    describe('Add Button to Card functionality', () => {
        it('Toolbox has at least one button in it', () => {
            let buttonViews = windowCurrentCardView.querySelectorAll('st-button');
            assert.isTrue(buttonViews.length > 0);
        });
        it('First button has a script set', () => {
            let firstButtonModel = windowCurrentCardView.querySelector('st-button').model;
            let script = firstButtonModel.partProperties.getPropertyNamed(
                firstButtonModel,
                'script'
            );

            assert.notEqual(script, '');
        });
        it('Current Card has no Buttons yet', () => {
            let cardModel = currentCardView.model;
            let buttonSubParts = cardModel.subparts.filter(subpart => {
                return subpart.type == 'button';
            });
            assert.equal(0, buttonSubParts.length);
        });
        it('Current CardView has no ButtonViews yet', () => {
            let buttonViews = Array.from(
                currentCardView.querySelectorAll('st-button')
            );
            assert.equal(0, buttonViews.length);
        });
        it('Clicking add buttons button twice adds two button models and views', () => {
            let addButtonButtonView = windowCurrentCardView.querySelector('st-button:first-child');
            assert.exists(addButtonButtonView);

            // Click twice
            addButtonButtonView.click();
            addButtonButtonView.click();

            // Check models
            let cardModel = currentCardView.model;
            let buttonModels = cardModel.subparts.filter(subpart => {
                return subpart.type == 'button';
            });
            assert.equal(buttonModels.length, 2);

            // Check views
            let buttonViews = currentCardView.querySelectorAll('st-button');
            assert.equal(buttonViews.length, 2);
        });
    });
});


<|start_filename|>js/objects/examples/older/wc1-entry.js<|end_filename|>
/**
 * Entrypoint for WC1 Example for Webpack
 */
import Part from '../parts/Part.js';
import PartView from '../views/PartView.js';
import WorldStack from '../parts/WorldStack.js';
import WorldView from '../views/WorldView.js';

document.addEventListener('DOMContentLoaded', () => {
    // Register the custom elements
    window.customElements.define('world-view', WorldView);
});


<|start_filename|>js/objects/parts/Field.js<|end_filename|>
/**
 * Field
 * -----------------------------------------
 * I am a Field Part.
 * I am a container that holds text. I also allow
 * a user to edit my text.
 */
import Part from './Part.js';
import {
    BasicProperty,
    DynamicProperty
} from '../properties/PartProperties.js';

import {
    addBasicStyleProps,
    addPositioningStyleProps,
    addTextStyleProps
} from '../utils/styleProperties.js';

const sides = ["top", "bottom", "left", "right"];

class Field extends Part {
    constructor(owner, name){
        super(owner);

        this.acceptedSubpartTypes = ["field"];

        this.isField = true;

        if(name){
            this.partProperties.setPropertyNamed(
                this,
                'name',
                name
            );
        }

        // Set the Field-specific
        // Part Properties
        this.partProperties.newBasicProp(
            'mode',
            'editing' //TODO this should be either "bravo" or "simpletalk"
        );

        this.partProperties.newBasicProp(
            'innerHTML',
            ''
        );

        this.partProperties.newBasicProp(
            'targetRangeId',
            null
        );

        // 'text' is a DynamicProperty configured to also set the innerHTML
        // BasicProperty when changed. The basic idea is that 'text' will be
        // the property that ST will interface with and everytime it
        // is changed the 'innerHTML' property should follow.
        this.partProperties.newDynamicProp(
            'text',
            (owner, prop, value, notify) => {
                prop._value = value;
                if(notify){
                    /*
                    if(!value){
                        value = "<br>";
                    }
                    // replace all newline characters with <br>
                    value = value.replace(/\n/g, "<br>");
                    */
                    owner.partProperties.setPropertyNamed(owner, 'innerHTML', value, notify);
                }
            },
            (owner, prop) => {
                return prop._value;
            },
            false, // not read only
            ''     // default is empty string
        );

        this.partProperties.newBasicProp(
            'editable',
            true
        );


        // A number of the props deal with direct text editing,
        // and so they are like commands. Examples include "undo"
        // "redo" "clear" etc. Here we use dynami props which the
        // view can respond to accordingly, but having these props have
        // no actual 'state'
        /** TODO: these should be private commands
        this.partProperties.newDynamicProp(
            "undo",
            () => {}, // all we is a notification
            () => {} // no getter
        );
        this.partProperties.newDynamicProp(
            "redo",
            () => {}, // all we is a notification
            () => {} // no getter
        );
        this.partProperties.newDynamicProp(
            "remove-format",
            () => {}, // all we is a notification
            () => {} // no getter
        );
        **/

        // Styling
        // setting width and height to null
        // effectively forces to the default size
        // of the button to fit the button name
        addBasicStyleProps(this);
        addPositioningStyleProps(this);
        addTextStyleProps(this);
        this.setupStyleProperties();
        // part specific default style properties
        this.partProperties.setPropertyNamed(
            this,
            'background-color',
            "rgb(255, 248, 220)", // var(--palette-cornsik)
            true, // notify
            true  // set default
        );
        sides.forEach((s) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${s}-width`,
                "medium",
                true, // notify
                true  // set default
            );
        });
        sides.forEach((s) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${s}-style`,
                "solid",
                true, // notify
                true  // set default
            );
        });
        sides.forEach((s) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${s}-color`,
                "black",
                true, // notify
                true  // set default
            );
        });
        sides.forEach((s) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${s}-width`,
                "1",
                true, // notify
                true  // set default
            );
        });
        this.partProperties.setPropertyNamed(
            this,
            "width",
            400,
            true, // notify
            true  // set default
        );

        // Private command handlers

        this.insertRange = this.insertRange.bind(this);
        this.setSelection = this.setSelection.bind(this);
        this.setPrivateCommandHandler("insertRange", this.insertRange);
        this.setPrivateCommandHandler("setSelection", this.setSelection);
        this.setPrivateCommandHandler("highlightSyntax", this.highlightSyntax);
        this.setPrivateCommandHandler("unhighlightSyntax", this.unhighlightSyntax);
    }

    insertRange(senders, rangeId, html, css){
        window.System.findViewsById(this.id).forEach((view) => {
            view.insertRange(rangeId, html, css);
        });
    }

    setSelection(senders, propertyName, propertyValue){
        // for now just allow properties of type "text-*" to be set
        if(propertyName.startsWith("text-")){
            window.System.findViewsById(this.id).forEach((view) => {
                view.setSelection(propertyName, propertyValue);
            });
        }
    }

    highlightSyntax(){
        let view = window.System.findViewById(this.id);
        if(view){
            view.highlightSyntax();
        }
    }

    unhighlightSyntax(){
        let view = window.System.findViewById(this.id);
        if(view){
            view.unhighlightSyntax();
        }
    }

    get type(){
        return 'field';
    }
};

export {
    Field,
    Field as default
};


<|start_filename|>js/objects/parts/Resource.js<|end_filename|>
import {Part} from './Part.js';
import {
    addBasicStyleProps,
    addPositioningStyleProps,
    addTextStyleProps
} from '../utils/styleProperties.js';

class Resource extends Part {
    constructor(owner, src) {
        super(owner);
        this.resource = null;

        // Properties
        this.partProperties.newBasicProp(
            "src",
            null
        );

        this.src = null;

        this.partProperties.newBasicProp(
            'readyState',
            "notReady"
        );

        this.partProperties.newBasicProp(
            "prerequisite",
            null
        );

        this.partProperties.newBasicProp(
            "resourceName",
            null
        );

        this.partProperties.newBasicProp(
            "response",
            null
        );

        // Private command handlers
        this.setPrivateCommandHandler("loadResource", this.loadResource);
        this.setPrivateCommandHandler("setSourceTo", this.setSourceTo);
        this.setPrivateCommandHandler("get", this.get);

        // Bind component methods
        this.loadResource = this.loadResource.bind(this);
        this.get = this.get.bind(this);
        this.reset = this.reset.bind(this);

        // load the src if provided
        if(src){
            this.partProperties.setPropertyNamed(this, "src", url);
        }
        // Style properties
        addBasicStyleProps(this);
        addPositioningStyleProps(this);
        addTextStyleProps(this);
        this.setupStyleProperties();
        this.partProperties.setPropertyNamed(
            this,
            'background-transparency',
            0,
            true, // notify
            true  // set default
        );
        ["right", "left", "top", "bottom"].forEach((side) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${side}-width`,
                1,
                true, // notify
                true  // set default
            );
        });
    }

    get type(){
        return 'resource';
    }

    loadResource(senders, resourceName){
        if(!window.System.availableResources || !window.System.availableResources[resourceName]){
            // TODO this should be a ST error
            throw Error(`resource ${resourceName} not found`);
        }
        this.resource = window.System.availableResources[resourceName];
        this.partProperties.setPropertyNamed(this, "resourceName", resourceName);
        // we can't guarantee the state of a resource and so it should be re-set every time
        this.reset();
    }

    setSourceTo(senders, sourceUrl){
        if(!this.resource){
            // TODO this should be a ST error
            throw Error(`no resource loaded for resource part id ${this.id}`);
        }
        this.partProperties.setPropertyNamed(this, "src", sourceUrl);
        this.resource.load(sourceUrl);
        // we can't guarantee the state of a resource and so it should be re-set every time
        this.reset();

    }

    get(senders, ...args){
        this.partProperties.setPropertyNamed(this, "readyState", "fetching");
        let prerequisite = this.partProperties.getPropertyNamed(this, "prerequisite");
        this.resource.get(prerequisite, ...args).then((response) => {
            this.partProperties.setPropertyNamed(this, "response", response);
            this.partProperties.setPropertyNamed(this, "readyState", "ready");
            this.sendMessage({
                type: "command",
                commandName: "responded",
                args: [this.id, this.resource.name],
                shouldIgnore: true
            }, this);
        }, (error) => {
            this.partProperties.setPropertyNamed(this, "readyState", "error");
            this.partProperties.setPropertyNamed(this, "response", null);
            this.sendMessage({
                type: "command",
                commandName: "errored",
                args: [this.id, this.resource.name, error.message],
                shouldIgnore: true
            }, this);
        });
    }

    reset(){
        this.partProperties.setPropertyNamed(this, "readyState", "notReady");
        this.partProperties.setPropertyNamed(this, "response", null);
    }
};

export {
    Resource,
    Resource as default
};


<|start_filename|>js/objects/tests/test-view-prop-handling-preload.js<|end_filename|>
/**
 * PartView Property Change Handler Testing
 * --------------------------------------------------
 * Test the ability of PartView descendants to properly
 * use the convenience methods for registering property
 * listeners from their models.
 */
import chai from 'chai';
const assert = chai.assert;
import PartView from '../views/PartView.js';
import Part from '../parts/Part.js';
import {
    addBasicStyleProps,
    addPositioningStyleProps,
    addLayoutStyleProps
} from '../utils/styleProperties.js';

// Create a test view and part combo
class TestView extends PartView {
    constructor(){
        super();
    }
};

class TestPart extends Part {
    constructor(){
        super();
        this.partProperties.newBasicProp(
            'test-property',
            false
        );
    }
};

// Register these with the system
System.registerPart('test', TestPart);
System.registerView('test', TestView);
window.customElements.define('st-test', TestView);

describe('Test use of #onPropChange registration', () => {
    let testView;
    let testModel;
    it('Can create View/Model pair', () => {
        testView = document.createElement('st-test');
        testModel = new TestPart();
        testView.setModel(testModel);
        assert.exists(testView);
        assert.exists(testModel);
        assert.equal(testModel, testView.model);
    });
    it('Can register prop change handler on view (adds to internal dict)', () => {
        let handler = function(newVal, partId){
            // 'this' context will be the view instance
            // when this function is called in reaction to
            // a property change on the view's model
            this.__val = newVal;
            this.__partId = partId;
        };
        testView.onPropChange('test-property', handler);
        assert.equal(
            testView.propChangeHandlers['test-property'],
            handler
        );
    });
    it('View reacts to test property change using handler', () => {
        testModel.partProperties.setPropertyNamed(
            testModel,
            'test-property',
            true
        );
        assert.isTrue(testView.__val); // See previous test
        assert.equal(
            testView.__partId, // See previous test
            testModel.id
        );
    });
});
describe('Test #onPropChange command delegation', () => {
    var buttonModel;
    let buttonView;
    var buttonPropertyName;
    var buttonPropertyValue;
    var ownerPropertyName;
    var ownerPropertyValue;
    before('Can create View/Model pair', () => {
        let stackEl = document.querySelector('.current-stack');
        let cardEl = stackEl.querySelector('st-card');
        let msg = {
            type: "command",
            commandName: "newModel",
            args: ["button", cardEl.model.id]
        };
        System.receiveMessage(msg);
        buttonView = cardEl.querySelector('st-button');
        assert.exists(buttonView);
        buttonModel = buttonView.model;
        assert.exists(buttonModel);
        // add a command propertyChanged command handler to the button
        buttonModel._commandHandlers["propertyChanged"] = function(_, name, value){
            buttonPropertyName = name;
            buttonPropertyValue = value;
        };
        // add a command propertyChanged command handler to the button owner
        buttonModel._owner._commandHandlers["propertyChanged"] = function(_, name, value){
            ownerPropertyName = name;
            ownerPropertyValue = value;
        };
    });
    it('A #propertyChanged message triggers a property change command + handler', () => {
        assert.equal(buttonPropertyName, null);
        assert.equal(buttonPropertyValue, null);
        let msg = {
            type: "propertyChanged",
            propertyName: "background-color",
            value: "red",
            partId: buttonModel.id
        };
        buttonModel.sendMessage(msg, buttonView);
        assert.equal(buttonPropertyName, "background-color");
        assert.equal(buttonPropertyValue, "red");
    });
    it('A #propertyChanged command does not delegate up the chain', () => {
        // remove the propertyChange handler from button; in general this should
        // delegate to its owner which is what we test not to happen
        buttonModel._commandHandlers = [];
        let msg = {
            type: "propertyChanged",
            propertyName: "background-color",
            value: "red",
            partId: buttonModel.id
        };
        buttonModel.sendMessage(msg, buttonView);
        assert.equal(ownerPropertyName, null);
        assert.equal(ownerPropertyValue, null);
    });
});

describe('Test wants-* partProperty handling', () => {
    let testView;
    let testModel;
    testView = document.createElement('st-test');
    testModel = new TestPart();
    addPositioningStyleProps(testModel);
    testView.setModel(testModel);
    let top = testModel.partProperties.getPropertyNamed(testModel, "top");
    let left = testModel.partProperties.getPropertyNamed(testModel, "left");
    describe('wants-move', () => {
        it('Initially set to false, mousedown has no effect', () => {
            let mousedownEvent = new window.MouseEvent('mousedown');
            let mousemoveEvent = new window.MouseEvent('mousemove');
            mousemoveEvent.movementX = 10;
            mousemoveEvent.movementY = 20;
            let mouseupEvent = new window.MouseEvent('mouseup');
            testView.dispatchEvent(mousedownEvent);
            testView.dispatchEvent(mousemoveEvent);
            testView.dispatchEvent(mouseupEvent);
            assert.equal(0, testModel.partProperties.getPropertyNamed(testModel, "top"));
            assert.equal(0, testModel.partProperties.getPropertyNamed(testModel, "left"));
        });
        it.skip('Setting to true, mousedown has appropriate effect', () => {
            testModel.partProperties.setPropertyNamed(testModel, "wants-move", true);
            let mousedownEvent = new window.MouseEvent('mousedown');
            let mousemoveEvent = new window.MouseEvent('mousemove');
            mousemoveEvent.movementX = 10;
            mousemoveEvent.movementY = 20;
            let mouseupEvent = new window.MouseEvent('mouseup');
            testView.dispatchEvent(mousedownEvent);
            testView.dispatchEvent(mousemoveEvent);
            testView.dispatchEvent(mouseupEvent);
            assert.equal(20, testModel.partProperties.getPropertyNamed(testModel, "top"));
            assert.equal(10, testModel.partProperties.getPropertyNamed(testModel, "left"));
        });
    });
});


<|start_filename|>js/objects/tests/test-if-then-with-preload.js<|end_filename|>
/**
 * Testing If-Then Statements
 * ----------------------------------
 * This module tests live if then statements
 * in real Part scripts.
 * It also implicitly tests the semantics
 * of the Conditionals defined within the
 * Simpletalk grammar/interpreter semantics
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

let currentCardModel;
let buttonModel;

function compileButtonScript(aScript){
    let msg = {
        type: 'compile',
        codeString: aScript,
        targetId: buttonModel.id
    };
    window.System.compile(msg);
}

describe('Model Setup', () => {
    it('Can find the current card model', () => {
        let currentCardView = document.querySelector('.current-stack > .current-card');
        currentCardModel = currentCardView.model;
        assert.exists(currentCardModel);
    });
    it('Can add a button to current card model without error', () => {
        let addButton = function(){
            let msg = {
                type: 'command',
                commandName: 'newModel',
                args: [
                    'button',
                    currentCardModel.id,
                    'card'
                ]
            };
            currentCardModel.sendMessage(msg, currentCardModel);
        };
        expect(addButton).to.not.throw(Error);
    });
    it('Can find newly created button model', () => {
        let button = currentCardModel.subparts.filter(subpart => {
            return subpart.type == 'button';
        })[0];
        buttonModel = button;
        assert.exists(buttonModel);
    });
});
describe('Inline if-then tests', () => {
    beforeEach(() => {
        buttonModel.partProperties.setPropertyNamed(
            buttonModel,
            'name',
            'blank'
        );
    });
    it('Can deal with a true literal', () => {
        let script = [
            'on click',
            '\tif true then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a false literal', () => {
        let script = [
            'on click',
            '\tif false then set "name" to "evaluated2"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated2');
    });
    it('Can deal with an equality comparison (true)', () => {
        let script = [
            'on click',
            '\tif (2+2) == 4 then set "name" to "evaluated3"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated3');
    });
    it('Can deal with an equality comparison (false)', () => {
        let script = [
            'on click',
            '\tif (2+3) == 4 then set "name" to "evaluated4"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated4');
    });
    it('Can deal with a gt comparison (true)', () => {
        let script = [
            'on click',
            '\tif 3 > 2 then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a gt comparison (false)', () => {
        let script = [
            'on click',
            '\tif 3 > 4 then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated');
    });
    it('Can deal with a gte comparison (true)', () => {
        let script = [
            'on click',
            '\tif 3 >= 3 then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a gte comparison (false)', () => {
        let script = [
            'on click',
            '\tif 3 >= 5.6 then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated');
    });
    it('Can deal with a lt comparison (true)', () => {
        let script = [
            'on click',
            '\tif -3 < 2 then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a lt comparison (false)', () => {
        let script = [
            'on click',
            '\tif 3 < 2 then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated');
    });
    it('Can deal with a lte comparison (true)', () => {
        let script = [
            'on click',
            'put 3 into myVariable',
            '\tif 3 <= myVariable then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a lte comparison (false)', () => {
        let script = [
            'on click',
            'put -0.15 into myVariable',
            '\tif 3 <= myVariable then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated');
    });
});
describe('Singleline If-Then tests', () => {
   beforeEach(() => {
        buttonModel.partProperties.setPropertyNamed(
            buttonModel,
            'name',
            'blank'
        );
    });
    it('Can deal with a true literal', () => {
        let script = [
            'on click',
            '\tif true',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a false literal', () => {
        let script = [
            'on click',
            '\tif false',
            'then set "name" to "evaluated2"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated2');
    });
    it('Can deal with an equality comparison (true)', () => {
        let script = [
            'on click',
            '\tif (2+2) == 4',
            'then set "name" to "evaluated3"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated3');
    });
    it('Can deal with an equality comparison (false)', () => {
        let script = [
            'on click',
            '\tif (2+3) == 4',
            'then set "name" to "evaluated4"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated4');
    });
    it('Can deal with a gt comparison (true)', () => {
        let script = [
            'on click',
            '\tif 3 > 2',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a gt comparison (false)', () => {
        let script = [
            'on click',
            '\tif 3 > 4',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated');
    });
    it('Can deal with a gte comparison (true)', () => {
        let script = [
            'on click',
            '\tif 3 >= 3',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a gte comparison (false)', () => {
        let script = [
            'on click',
            '\tif 3 >= 5.6',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated');
    });
    it('Can deal with a lt comparison (true)', () => {
        let script = [
            'on click',
            '\tif -3 < 2',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a lt comparison (false)', () => {
        let script = [
            'on click',
            '\tif 3 < 2',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated');
    });
    it('Can deal with a lte comparison (true)', () => {
        let script = [
            'on click',
            'put 3 into myVariable',
            '\tif 3 <= myVariable',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluated');
    });
    it('Can deal with a lte comparison (false)', () => {
        let script = [
            'on click',
            'put -0.15 into myVariable',
            '\tif 3 <= myVariable',
            'then set "name" to "evaluated"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.notEqual(foundName, 'evaluated');
    }); 
});
describe('Singleline if-then-else tests', () => {
    beforeEach(() => {
        buttonModel.partProperties.setPropertyNamed(
            buttonModel,
            'name',
            'blank'
        );
    });
    it('Can deal with a true literal', () => {
        let script = [
            'on click',
            '\tif true',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedTrue');
    });
    it('Can deal with a false literal', () => {
        let script = [
            'on click',
            '\tif false',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedFalse');
    });
    it('Can deal with an equality comparison (true)', () => {
        let script = [
            'on click',
            '\tif (2 + -4) is -2',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedTrue');
    });
    it('Can deal wtih an equality comparison (false)', () => {
        let script = [
            'on click',
            '\tif 2 is 3',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedFalse');
    });
    it('Can deal with a gt comparison (true)', () => {
        let script = [
            'on click',
            '\tif (2 + 3) > 1',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedTrue');
    });
    it('Can deal with a gt comparison (false)', () => {
        let script = [
            'on click',
            '\tif (2 + 3) > 6',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedFalse');
    });
    it('Can deal with a gte comparison (true)', () => {
        let script = [
            'on click',
            '\tif 2 >= (1 + 1)',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedTrue');
    });
    it('Can deal with a gte comparison (false)', () => {
        let script = [
            'on click',
            '\tif 2 >= 44.06',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedFalse');
    });
    it('Can deal with a lt comparison (true)', () => {
        let script = [
            'on click',
            '\tif (-1 + 1) < 1',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedTrue');
    });
    it('Can deal with a lt comparison (false)', () => {
        let script = [
            'on click',
            '\tif (-1 + 1) < 0',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedFalse');
    });
    it('Can deal with a lte comparison (true)', () => {
        let script = [
            'on click',
            '\tif (1 + -1) <= 0',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedTrue');
    });
    it('Can deal with a lte comparison (false)', () => {
        let script = [
            'on click',
            '\tif (1 + -1) <= -1',
            '\tthen set "name" to "evaluatedTrue"',
            '\telse set "name" to "evaluatedFalse"',
            'end click'
        ].join('\n');
        compileButtonScript(script);
        let clickMessage = {
            type: 'command',
            commandName: 'click',
            args: []
        };
        buttonModel.sendMessage(clickMessage, buttonModel);
        let foundName = buttonModel.partProperties.getPropertyNamed(buttonModel, 'name');
        assert.equal(foundName, 'evaluatedFalse');
    });
});


<|start_filename|>js/ohm/tests/test-arbitrary-command-grammar.js<|end_filename|>
/**
 * Tests dealing with user-defined commands
 * and messages
 */
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
}
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
}
const matchAndsemanticMatchTest = (str, semanticType) => {
    matchTest(str);
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
}
const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
}
String.prototype.unCapitalize = function() {
    return this.charAt(0).toLowerCase() + this.slice(1);
}


describe('Basic Definitions', () => {
    it('Can understand an arbitrary handler definition with no args', () => {
        let str = `on myCustomCommand\n\tanswer "hello"\nend myCustomCommand`;
        semanticMatchTest(str, 'MessageHandler');
    });
    it('Can understand an arbitrary handler definition with 1 argument', () => {
        let str = `on myCustomCommand firstArg\n\tanswer firstArg\nend myCustomCommand`;
        semanticMatchTest(str, 'MessageHandler');
    });
    it('Can understand an arbitrary handler definition with 2 arguments', () => {
        let str = `on myCustomCommand firstArg, secondArg\n\tanswer secondArg\nend myCustomCommand`;
        semanticMatchTest(str, 'MessageHandler');
    });
    it('Can understand an arbitrary handler definition with 3 arguments', () => {
        let str = `on myCustomCommand firstArg, secondArg, thirdArg\n\tanswer thirdArg\nend myCustomCommand`;
        semanticMatchTest(str, 'MessageHandler');
    });
});

describe('Basic Arbitrary Message Statments (ie used handler bodies)', () => {
    it('Can parse a simple message statement with no arguments', () => {
        let str = "myCustomCommand";
        semanticMatchTest(str, "Command");
        semanticMatchTest(str, "Command_arbitraryCommand");
    });
    it('Can parse a simple message statement with one argument (a variable)', () => {
        let str = "myCustomCommand someArgument";
        semanticMatchTest(str, "Command");
        semanticMatchTest(str, "Command_arbitraryCommand");
    });
    it('Can parse a simple message statement with two arguments (both variables)', () => {
        let str = "myCustomCommand someArgument, someOtherArgument";
        semanticMatchTest(str, "Command");
        semanticMatchTest(str, "Command_arbitraryCommand");
    });
    it('Can parse a simple message statement with three arguments (all variables)', () => {
        let str = "myCustomCommand someArg, anotherArg, aThirdArg";
        semanticMatchTest(str, "Command");
        semanticMatchTest(str, "Command_arbitraryCommand");
    });
});

describe("Arbitrary Message Statements with literal arguments", () => {
    describe('String literals', () => {
        it('with one argument', () => {
            let str = `myCustomMessage "hello"`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with two arguments', () => {
            let str = `myCustomMessage "hello", "world"`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with three arguments', () => {
            let str = `myCustomMessage "hello", "world", "Again!"`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
    });
    describe('Integer literals', () => {
        it('with one argument, positive', () => {
            let str = `myCustomCommand 234`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with one argument, negative', () => {
            let str = `myCustomCommand -1123`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with two arguments, positive', () => {
            let str = `myCustomCommand 1231234, 345`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with two arguments, negative', () => {
            let str = `myCustomCommand -12355656, -4`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with three arguments, positive', () => {
            let str = `myCustomCommand 9079, 1, 59`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with three arguments, negative', () => {
            let str = `myCustomCommand -3, -999, -234587`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
    });
    describe('Float literals', () => {
        it('with one argument, positive', () => {
            let str = `myCustomCommand 0.2`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with one argument, negative', () => {
            str = `myCustomCommand -0.034`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with two arguments, positive', () => {
            let str = `myCustomCommand 2435234453.2, 0.00001`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with two arguments, negative', () => {
            let str = `myCustomCommand -00.0001, -2342345.8`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with three arguments, positive', () => {
            let str = `myCustomCommand 0.4, 1111.03, 99.9`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('with three arguments, negative', () => {
            let str = `myCustomCommand -0.4, -1111.03, -99.9`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
    });
});

describe('Arbitrary Message Statements with Expressions as Args', () => {
    describe('One argument statements', () => {
        it('Addition Expression', () => {
            let str = `myCustomCommand 1 + 2`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('Long addition expression', () => {
            let str = `myCustomCommand 1 + 2 + 3 + 55 + -0.1`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('Multiplication expression', () => {
            let str = `myCustomCommand 2 * -3.1`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
    });
    describe('Two argument statements', () => {
        it('Addition expressions', () => {
            let str = `myCustomCommand 1 + 2, 55 + -0.14`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
        it('Multiplication expressions', () => {
            let str = `myCustomCommand 12 * 100, -3.7 * 5`;
            semanticMatchTest(str, "Command");
            semanticMatchTest(str, "Command_arbitraryCommand");
        });
    });
});

describe('Example Custom Script', () => {
    let script = [
        'on myCustomMessage firstArg, secondArg, thirdArg',
        '\tanswer firstArg',
        '\tanswer secondArg',
        '\nanswer thirdArg',
        'end myCustomMessage',
        '',
        'on click',
        '\tmyCustomMessage -0.3, 2 * 9, someVariableName',
        'end click'
    ].join("\n");
    it('Can parse the example script without error', () => {
        matchTest(script);
    });
});


<|start_filename|>js/objects/parts/Stack.js<|end_filename|>
/**
 * Stack
 * ----------------------------
 * I am the Stack Part.
 * I represent a collection of Card parts,
 * along with some extra configurability.
 */
import Part from './Part.js';
import Card from './Card.js';
import {
    BasicProperty
} from '../properties/PartProperties.js';

class Stack extends Part {
    constructor(owner, name, deserializing=false){
        super(owner);
        this.acceptedSubpartTypes = [
            "card", "window", "button", "area", "field",
            "drawing", "image", "audio", "browser"
        ];

        // Set up Stack specific
        // PartProperties
        this.partProperties.newBasicProp(
            'cantPeek',
            false
        );
        this.partProperties.newBasicProp(
            'resizable',
            false
        );


        // Will hold the card-based index,
        // which here is zero-indexed, of the
        // card that is the current card for this
        // Stack.
        this.partProperties.newBasicProp(
            'current',
            null
        );

        // Bind general methods
        this.sendOpenCardTo = this.sendOpenCardTo.bind(this);
        this.sendCloseCardTo = this.sendCloseCardTo.bind(this);

        // Bind stack navigation methods
        this.goToNextCard = this.goToNextCard.bind(this);
        this.goToPrevCard = this.goToPrevCard.bind(this);
        this.goToCardById = this.goToCardById.bind(this);
        this.goToNthCard = this.goToNthCard.bind(this);

        // remove the halo property and command handlers as these do not makes sense here
        let haloOpenProp = this.partProperties.findPropertyNamed("halo-open");
        if(haloOpenProp){
            this.partProperties.removeProperty(haloOpenProp);
        }
        this.removePrivateCommandHandler("openHalo");
        this.removePrivateCommandHandler("closeHalo");
    }

    goToNextCard(){
        let cards = this.subparts.filter(subpart => {
            return subpart.type == 'card';
        });
        if(cards.length < 2){
            return;
        }
        let currentCardId = this.currentCardId;
        let currentCard = this.currentCard;
        let currentIdx = cards.indexOf(currentCard);
        let nextIdx = currentIdx + 1;
        if(nextIdx >= cards.length){
            nextIdx = (nextIdx % cards.length);
        }
        let nextCard = cards[nextIdx];
        this.partProperties.setPropertyNamed(
            this,
            'current',
            nextCard.id
        );
        if(currentCardId != nextCard.id){
            this.sendCloseCardTo(currentCard);
            this.sendOpenCardTo(nextCard);
        }
    }

    goToCardById(anId){
        let currentCardId = this.currentCardId;
        let currentCard = this.currentCard;
        let cards = Object.values(window.System.partsById).filter((part) => {
            return part.type == "card";
        });
        let nextCard = cards.find(card => {
            return card.id == anId;
        });
        if(!nextCard){
            throw new Error(`The card id: ${anId} cant be found stack`);
        }
        // if the card is not on this stack we should go to the corresponding stack
        if(nextCard._owner != this){
            this._owner.goToStackById(nextCard._owner.id);
        }
        nextCard._owner.partProperties.setPropertyNamed(
            nextCard._owner,
            'current',
            nextCard.id
        );
        if(currentCardId != nextCard.id){
            this.sendCloseCardTo(currentCard);
            this.sendOpenCardTo(nextCard);
        }
    }

    goToPrevCard(){
        let cards = this.subparts.filter(subpart => {
            return subpart.type == 'card';
        });
        if(cards.length < 2){
            return;
        }
        let currentCardId = this.currentCardId;
        let currentCard = this.currentCard;
        let currentIdx = cards.indexOf(currentCard);

        let nextIdx = currentIdx - 1;
        if(nextIdx < 0){
            nextIdx = cards.length + nextIdx;
        }
        let nextCard = cards[nextIdx];
        this.partProperties.setPropertyNamed(
            this,
            'current',
            nextCard.id
        );
        if(currentCardId != nextCard.id){
            this.sendCloseCardTo(currentCard);
            this.sendOpenCardTo(nextCard);
        }
    }

    goToNthCard(anIndex){
        // NOTE: We are using 1-indexed values
        // per the SimpleTalk system
        let trueIndex = anIndex - 1;
        let cards = this.subparts.filter(subpart => {
            return subpart.type == 'card';
        });
        if(trueIndex < 0 || trueIndex > cards.length -1){
            console.warn(`Cannot navigate to card number ${anIndex} -- out of bounds`);
            return;
        }
        let currentCardId = this.currentCardId;
        let currentCard = this.currentCard;
        let nextCard = cards[trueIndex];
        this.partProperties.setPropertyNamed(
            this,
            'current',
           nextCard.id
        );
        if(currentCardId != nextCard.id){
            this.sendCloseCardTo(currentCard);
            this.sendOpenCardTo(nextCard);
        }
    }

    sendCloseCardTo(aCard){
        this.sendMessage(
            {
                type: 'command',
                commandName: 'closeCard',
                args: [],
                shouldIgnore: true
            },
            aCard
        );
    }

    sendOpenCardTo(aCard){
        this.sendMessage(
            {
                type: 'command',
                commandName: 'openCard',
                args: [],
                shouldIgnore: true
            },
            aCard
        );
    }

    get type(){
        return 'stack';
    }

    get currentCardId(){
        return this.partProperties.getPropertyNamed(
            this,
            'current'
        );
    }

    get currentCard(){
        return window.System.partsById[this.currentCardId];
    }

    // override the base class methods
    moveSubpartDown(part){
        let currentIndex = this.subparts.indexOf(part);
        let lastValidPartIndex = this.subparts.length - 1;
        if(part.type == "card"){
            let allCards = this.subparts.filter((part) => {
                return part.type == "card";
            });
            lastValidPartIndex = allCards.length - 1;
        }
        if(currentIndex < lastValidPartIndex){
            this.subpartOrderChanged(part.id, currentIndex, currentIndex + 1);
        }
    }

    moveSubpartToLast(part){
        let currentIndex = this.subparts.indexOf(part);
        let lastValidPartIndex = this.subparts.length - 1;
        if(part.type == "card"){
            let allCards = this.subparts.filter((part) => {
                return part.type == "card";
            });
            lastValidPartIndex = allCards.length - 1;
        }
        if(currentIndex < lastValidPartIndex){
            this.subpartOrderChanged(part.id, currentIndex, lastValidPartIndex);
        }
    }

    moveSubpartUp(part){
        let currentIndex = this.subparts.indexOf(part);
        let firstValidPartIndex = 0;
        if(part.type != "card"){
            let allCards = this.subparts.filter((part) => {
                return part.type == "card";
            });
            firstValidPartIndex = allCards.length;
        }
        if(currentIndex > firstValidPartIndex){
            this.subpartOrderChanged(part.id, currentIndex, currentIndex - 1);
        }
    }

    moveSubpartToFirst(part){
        let currentIndex = this.subparts.indexOf(part);
        let firstValidPartIndex = 0;
        if(part.type != "card"){
            let allCards = this.subparts.filter((part) => {
                return part.type == "card";
            });
            firstValidPartIndex = allCards.length;
        }
        if(currentIndex > firstValidPartIndex){
            this.subpartOrderChanged(part.id, currentIndex, firstValidPartIndex);
        }
    }

    addPart(aPart){
        if(!this.acceptsSubpart(aPart.type)){
            throw new Error(`${this.type} does not accept subparts of type ${aPart.type}`);
        }

        let found = this.subparts.indexOf(aPart);
        if(found < 0){
            // if the part is a card then append after the last card
            if(aPart.type == "card"){
                let allCards = this.subparts.filter((part) => {
                    return part.type == "card";
                });
                this.subparts.splice(allCards.length, 0, aPart);
            } else {
                this.subparts.push(aPart);
            }
            aPart._owner = this;
        }
    }
};

export {
    Stack,
    Stack as default
};


<|start_filename|>js/objects/tests/test-subparts.js<|end_filename|>
/**
 * Window Part Tests
 * -----------------------------------
 * Basic tests of the Window Part.
 * Here we are just ensuring basic validity
 * checks and property setting
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

// Import valid subpart kinds
import WorldStack from '../parts/WorldStack';
import Stack from '../parts/Stack.js';
import Card from '../parts/Card.js';
import Background from '../parts/Background.js';
import Button from '../parts/Button.js';
import Field from '../parts/Field.js';
import Image from '../parts/Image.js';
import Drawing from '../parts/Drawing.js';
import Audio from '../parts/Audio.js';
import Area from '../parts/Area.js';
import Window from '../parts/Window.js';


describe('Subpart Validity Tests', () => {
    describe('Card', () => {
        it('Accepts Button', () => {
            let button = new Button();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(button.type));
        });
        it('Accepts Field', () => {
            let field = new Field();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(field.type));
        });
        it('Accepts Image', () => {
            let image = new Image();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(image.type));
        });
        it('Accepts Drawing', () => {
            let drawing = new Drawing();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(drawing.type));
        });
        it('Accepts Audio', () => {
            let audio = new Audio();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(audio.type));
        });
        it('Accepts Window', () => {
            let win = new Window();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(win.type));
        });
        it('Rejects Stack', () => {
            let stack = new Stack();
            let card = new Card();
            assert.isFalse(
                card.acceptsSubpart(stack.type)
            );
        });
        it('Rejects World', () => {
            let world = new WorldStack();
            let card = new Card();
            assert.isFalse(
                card.acceptsSubpart(world.type)
            );
        });
        it('Rejects Background', () => {
            let background = new Background();
            let card = new Card();
            assert.isFalse(
                card.acceptsSubpart(background.type)
            );
        });
    });

    describe('Stack', () => {
        it('Accepts Card', () => {
            let card = new Card();
            let stack = new Stack();
            assert.isTrue(stack.acceptsSubpart(card.type));
        });
        it('Accepts Button', () => {
            let button = new Button();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(button.type));
        });
        it.skip('Accepts Image', () => {
            let image = new Image();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(image.type));
        });
        it.skip('Accepts Audio', () => {
            let audio = new Audio();
            let card = new Card();
            assert.isTrue(card.acceptsSubpart(audio.type));
        });
        it('Accepts Window', () => {
            let win = new Window();
            let stack = new Stack();
            assert.isTrue(stack.acceptsSubpart(win.type));
        });
        it('Accepts Field', () => {
            let ericField = new Field();
            let stack = new Stack();
            assert.isTrue(stack.acceptsSubpart(ericField.type));
        });
        it('Rejects World', () => {
            let world = new WorldStack();
            let stack = new Stack();
            assert.isFalse(stack.acceptsSubpart(world.type));
        });
    });

    describe('Button', () => {
        it('Rejects Card', () => {
            let card = new Card();
            let button = new Button();
            assert.isFalse(button.acceptsSubpart(card.type));
        });
        it('Rejects Field', () => {
            let ericField = new Field();
            let button = new Button();
            assert.isFalse(button.acceptsSubpart(ericField.type));
        });
        it('Rejects World', () => {
            let world = new WorldStack();
            let button = new Button();
            assert.isFalse(button.acceptsSubpart(world.type));
        });
        it('Rejects Stack', () => {
            let stack = new Stack();
            let button = new Button();
            assert.isFalse(button.acceptsSubpart(stack.type));
        });
        it('Rejects Window', () => {
            let win = new Window();
            let button = new Button();
            assert.isFalse(button.acceptsSubpart(win.type));
        });
    });

    describe('WorldStack', () => {
        it('Accepts Stack', () => {
            let stack = new Stack();
            let world = new WorldStack();
            assert.isTrue(world.acceptsSubpart(stack.type));
        });
        it('Rejects Card', () => {
            let card = new Card();
            let world = new WorldStack();
            assert.isFalse(world.acceptsSubpart(card.type));
        });
        it('Rejects Window', () => {
            let win = new Window();
            let world = new WorldStack();
            assert.isFalse(world.acceptsSubpart(win.type));
        });
        it('Rejects Button', () => {
            let button = new Button();
            let world = new WorldStack();
            assert.isFalse(world.acceptsSubpart(button.type));
        });
        it('Rejects Field', () => {
            let ericField = new Field();
            let world = new WorldStack();
            assert.isFalse(world.acceptsSubpart(ericField.type));
        });
    });

    describe('Field', () => {
        it('Rejects Card', () => {
            let card = new Card();
            let field = new Field();
            assert.isFalse(field.acceptsSubpart(card.type));
        });
        it('Rejects Button', () => {
            let button = new Button();
            let field = new Field();
            assert.isFalse(field.acceptsSubpart(button.type));
        });
        it('Rejects World', () => {
            let world = new WorldStack();
            let field = new Field();
            assert.isFalse(field.acceptsSubpart(world.type));
        });
        it('Rejects Stack', () => {
            let stack = new Stack();
            let field = new Field();
            assert.isFalse(field.acceptsSubpart(stack.type));
        });
        it('Rejects Window', () => {
            let win = new Window();
            let field = new Field();
            assert.isFalse(field.acceptsSubpart(win.type));
        });
    });

    describe('Window', () => {
        it('Rejects World', () => {
            let world = new WorldStack();
            let window = new Window();
            assert.isFalse(window.acceptsSubpart(world.type));
        });
        it('Rejects Stack', () => {
            let stack = new Stack();
            let window = new Window();
            assert.isFalse(window.acceptsSubpart(stack.type));
        });
        it('Rejects Card', () => {
            let card = new Card();
            let window = new Window();
            assert.isFalse(window.acceptsSubpart(card.type));
        });
        it('Rejects Window', () => {
            let win = new Window();
            let window = new Window();
            assert.isFalse(window.acceptsSubpart(win.type));
        });
        it('Accepts Area', () => {
            let area = new Area();
            let window = new Window();
            assert.isTrue(window.acceptsSubpart(area.type));
        });
    });
});


<|start_filename|>js/objects/views/editors/EditorPropList.js<|end_filename|>
import EditorPropItem from './EditorPropItem.js';

// PREAMBLE
window.customElements.define('editor-prop-item', EditorPropItem);

const templateString = `
<style>
    :host {
        display: flex;
        flex-direction: column;
        width: 100%;
        height: 100%;
        font-family: 'Helvetica', sans-serif;
        font-size: 0.8rem;
    }

    #props-list {
        flex: 1;
        overflow-y: auto;
        list-style: none;
        margin: 0;
        padding: 0;
        overflow-y: auto;
    }
    #filter-area {
        display: flex;
        width: 100%;
        align-items: center;
    }
    #filter-area > input {
        min-width: 0;
        width: auto;
        flex: 1;
        outline: none;
        font-size: 1.0rem;
        padding-left: 6px;
        padding-right: 6px;
        padding-top: 3px;
        padding-bottom: 3px;
        border: 1px solid rgba(100, 100, 100, 0.8);
        border-radius: 2px;
    }
</style>
<div id="filter-area">
    <input type="text" id="filter-input" name="filter-input" placeholder="Filter..."/>
    <button id="clear">Clear</button>
</div>
<ul id="props-list">
    <slot></slot>
</ul>
`;

const specialProps = [
    'cssStyle',
    'cssTextStyle',
    'id',
    'name',
    'target',
    'events',
    'current',
    'script',
    'custom-properties'
];

class EditorPropList extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Bound methods
        this.render = this.render.bind(this);
        this.onInput = this.onInput.bind(this);
        this.onFilterClearClick = this.onFilterClearClick.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.filterInputElement = this._shadowRoot.getElementById('filter-input');
            this.clearButton = this._shadowRoot.getElementById('clear');
            this.clearButton.addEventListener('click', this.onFilterClearClick);
        }
    }

    render(aModel){
        this.model = aModel;

        // Clear any existing main DOM children
        this.innerHTML = "";
        let inputEl = this._shadowRoot.getElementById('filter-input');
        inputEl.removeEventListener('input', this.onInput);
        inputEl.addEventListener('input', this.onInput);

        // Create a sorted copy of the property objects
        this.propList = this.model.partProperties.all.slice().filter(prop => {
                return !specialProps.includes(prop.name);
            });
        this.propList
            .sort((first, second) => {
            return first.name.localeCompare(second.name);
        });

        // Render the list item elements and insert them
        this.propList.forEach(propObject => {
            let el = document.createElement('editor-prop-item');
            el.setProperty(propObject, this.model);
            el.setAttribute('name', propObject.name);
            this.appendChild(el);
        });
    }

    filterBy(text){
        // Find all of the prop item elements whose
        // property name does *not* include the substring,
        // and set those to not display
        let allElements = Array.from(this.querySelectorAll('editor-prop-item'));
        allElements.forEach(propEl => {
            let name = propEl.getAttribute('name');
            if(name.toLowerCase().includes(text)){
                propEl.classList.remove('item-hidden');
            } else {
                propEl.classList.add('item-hidden');
            }
        });
    }

    onInput(event){
        this.filterBy(event.target.value.toLowerCase());
    }

    onFilterClearClick(event){
        this.filterInputElement.value = "";
        this.filterBy("");
    }
};

export {
    EditorPropList,
    EditorPropList as default
};


<|start_filename|>js/objects/views/editors/EditorTab.js<|end_filename|>
// PREAMBLE

const templateString = `
<style>
    :host {
        display: flex;
        flex: 1;
        align-items: center;
        justify-content: center;
        padding: 6px;
        opacity: 0.5;
        border-bottom: 2px solid rgba(100, 100, 100, 0.7);
        transition: border 0.2s linear, opacity 0.2s linear;
        user-select: none;
    }

    :host([active="true"]){
        border-bottom: 2px solid rgba(200, 0, 0, 0.9);
        opacity: 1.0;
        transition: border 0.2s linear, opacity 0.2s linear;
    }

    :host(:hover){
        cursor: pointer;
    }
</style>
<span id="label">
    <slot></slot>
</span>
`;

class EditorTab extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Bound methods
        this.onClick = this.onClick.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.addEventListener('click', this.onClick);

            // If the tab is currently activated, emit
            // the tab-activaed message
            if(this.getAttribute('active') == "true"){
                let event = new CustomEvent("tab-activated", {
                    bubbles: true
                });
                this.dispatchEvent(event);
            }
        }
    }

    disconnectedCallback(){
        this.removeEventListener('click', this.onClick);
    }

    onClick(event){
        let isActive = (this.getAttribute('active') == "true");
        if(!isActive){
            this.setAttribute('active', 'true');
            let event = new CustomEvent("tab-activated", {
                bubbles: true
            });
            this.dispatchEvent(event);
        }
    }
};

export {
    EditorTab,
    EditorTab as default
};


<|start_filename|>js/objects/tests/test-lifecycle-scripting-with-preload.js<|end_filename|>
/**
 * Lifecycle Scripting Preload Tests
 * -----------------------------------------
 * This file tests scriptable lifecycle handlers,
 * like openCard, openStack, openWorld, etc.
 */
import chai from 'chai';
import ohm from 'ohm-js';
const assert = chai.assert;
const expect = chai.expect;

function compileScriptForPart(aScript, aPart){
    let msg = {
        type: 'compile',
        targetId: aPart.id,
        codeString: aScript
    };
    aPart.sendMessage(msg, aPart);
}

describe("Lifecycle Scripting Tests", () => {
    describe("Setup", () => {
        it("There is one stack in the world", () => {
            let world = System.partsById['world'];
            let stacks = world.subparts.filter(subpart => {
                return subpart.type == 'stack';
            });
            assert.equal(stacks.length, 1);
        });
        it("We can add another stack to the World without issue", () => {
            let addStackAction = function(){
                System.newModel(
                    'stack',
                    'world'
                );
            };
            expect(addStackAction).to.not.throw();
        });
        it("There are now two stacks in the world", () => {
            let world = System.partsById['world'];
            let stacks = world.subparts.filter(subpart => {
                return subpart.type == 'stack';
            });
            assert.equal(stacks.length, 2);
        });
        it("We can add a card to the first stack without issue", () => {
            let firstStack = System.getCurrentStackModel();
            let addCardAction = function(){
                System.newModel('card', firstStack.id);
            };
            expect(addCardAction).to.not.throw();
            let cards = firstStack.subparts.filter(subpart => {
                return subpart.type == 'card';
            });
            assert.equal(cards.length, 2);
        });
        it("Can compile test script for first card of first stack", () => {
            let firstCard = System.getCurrentCardModel();
            let script = [
                'on openCard',
                '\tset "name" to "Card Opened"',
                'end openCard',
                '\n',
                'on closeCard',
                '\tset "name" to "Card Closed"',
                'end openCard'
            ].join('\n');
            let compileAction = function(){
                compileScriptForPart(script, firstCard);
            };
            expect(compileAction).to.not.throw();
        });
        it("Can compile test script for first stack", () => {
            let firstStack = System.getCurrentStackModel();
            let script = [
                'on openStack',
                '\tset "name" to "Stack Opened"',
                'end openStack',
                '\n',
                'on closeStack',
                '\tset "name" to "Stack Closed"',
                'end closeStack'
            ].join('\n');
            let compileAction = function(){
                compileScriptForPart(script, firstStack);
            };
            expect(compileAction).to.not.throw();
        });
        it("Can compile test script for world", () => {
            let world = System.partsById['world'];
            let script = [
                'on openWorld',
                '\tset "name" to "World Opened"',
                'end openWorld'
            ].join('\n');
            let compileAction = function(){
                compileScriptForPart(script, world);
                world.partProperties.setPropertyNamed(
                    world,
                    'script',
                    script
                );
                System.serialize();
            };
            expect(compileAction).to.not.throw();
        });
    });
    describe("openCard and closeCard tests", () => {
        let stack;
        let firstCard;
        before(() => {
            let world = System.partsById['world'];
            stack = world.subparts.filter(subpart => {
                return subpart.type == 'stack';
            })[0];
            firstCard = stack.subparts.filter(subpart => {
                return subpart.type == 'card';
            })[0];
        });
        // We have openCard and closeCard handlers now
        // attached to the first card of the first stack.
        // navigating to the second card should change the
        // card's name to 'Card Closed' and then navigating back to
        // it should change its name to 'Card Opened'
        it("closeCard triggered when navigating away from card", () => {
            stack.goToNextCard();
            let name = firstCard.partProperties.getPropertyNamed(
                firstCard,
                'name'
            );
            assert.equal(name, 'Card Closed');
        });
        it("openCard triggered when navigating to card", () => {
            stack.goToNextCard();
            let name = firstCard.partProperties.getPropertyNamed(
                firstCard,
                'name'
            );
            assert.equal(name, 'Card Opened');
        });
    });
    describe("openStack and closeStack tests", () => {
        let firstStack;
        let world;
        before(() => {
            world = System.partsById['world'];
            firstStack = world.subparts.filter(subpart => {
                return subpart.type == 'stack';
            })[0];
        });
        // We have openStack and closeStack handlers now
        // attached to the first stack of the world.
        // Navigating to the second stack should change the
        // first's name to 'Stack Closed' and then navigating
        // back to it should change its name to 'Stack Opened'
        it('closeStack triggered when navigating away from stack', () => {
            world.goToNextStack();
            let name = firstStack.partProperties.getPropertyNamed(
                firstStack,
                'name'
            );
            assert.equal(name, 'Stack Closed');
        });
        it('openStack triggered when navigating to the stack', () => {
            world.goToNextStack();
            let name = firstStack.partProperties.getPropertyNamed(
                firstStack,
                'name'
            );
            assert.equal(name, 'Stack Opened');
        });
    });
    describe("openWorld will be triggered", () => {
        // We manually call System.initialLoad to try and
        // trigger the world opening.
        let world;
        before(() => {
            world = System.partsById['world'];
        });
        it("World does not have a new name yet (openWorld not yet triggered)", () => {
            let name = world.partProperties.getPropertyNamed(
                world,
                'name'
            );
            assert.equal(name, 'New world');
        });
        it("Has the openWorld name change after System.initialLoad", (done) => {
            System.initialLoad();
            // We need to wait a little bit because the deserialization
            // in initialLoad is now an async (promise) operation
            setTimeout(() => {
                world = System.partsById['world']; // Refer to new object
                let name = world.partProperties.getPropertyNamed(
                    world,
                    'name'
                );
                assert.equal(name, 'World Opened');
                done();
            }, 1800);
        });
    });
});


<|start_filename|>developer/devtools/background.js<|end_filename|>
/*
 * The background script handles communication to and from
 * the content script, embedded in the document, and
 * the panel scripts, living in devtools.
 * These communiccation are handled by browser.runtime connection
 * ports.
 * The connections are initialized int he content and panel scripts,
 * respectively. Here we listen for these connection and create
 * connection/port specific handlers.
 */
var portFromCS;
var portFromPanel;

function connected(port) {
    // handle all communication to and from the panel
    if (port.name === "port-from-panel"){
        portFromPanel = port;
        // at the moment we don't do anything with messages coming
        // from the panels
        portFromPanel.onMessage.addListener(function(msg) {
            console.log("recieved message from panel", msg);
        });
    };
    // handle all communication to and from the content script
    if (port.name === "port-from-cs"){
        portFromCS = port;
        portFromCS.onMessage.addListener(function(msg) {
            // Having received a message from the content script, i.e.
            // from the target window we forward this message to the panels
            // if the connection is not alive we log this in the devtools's
            // debugger console
            notifyDevtoolsPabel(msg.data);
        });
    }
    // notify if the port has disconnected
    port.onDisconnect.addListener(function(port) {
        if (port.name === "port-from-panel" || port.name === "port-from-cs"){
            console.log(`${port.name}} has disconnected`);
        };
    });
}

browser.runtime.onConnect.addListener(connected);

function notifyDevtoolsPabel(msg){
    if (portFromPanel){
        portFromPanel.postMessage(msg);
    } else {
        console.log("failed to send message to devtools panel: port disconnected");
    }
}

// browser.browserAction.onClicked.addListener(function() {
//     portFromCS.postMessage({greeting: "they clicked the button!"});
// });


<|start_filename|>js/objects/utils/handInterface.js<|end_filename|>
const video = document.createElement('video');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

const scaleDim = (dim) => {
    const scale = 0.7;
    const stride = 16;
    const evenRes = dim * scale - 1;
    return evenRes - (evenRes % stride) + 1;
};

const detectHands = async () => {
    if (!handInterface.handDetectionRunning) {
        return;
    }
    const scaledWidth = scaleDim(canvas.width);
    const scaledHeight = scaleDim(canvas.height);
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    const image = tf.tidy(() => {
        return tf.fromPixels(canvas).resizeBilinear([scaledHeight, scaledWidth]).expandDims(0);
    });
    const [scores, tboxes] = await handInterface.handDetectionModel.executeAsync(image);
    image.dispose();
    const handsDetected = tf.tidy(() => {
        const indices = tf.image.nonMaxSuppression(
            tboxes.reshape([tboxes.shape[1], tboxes.shape[3]]),
            scores.reshape([scores.shape[1]]),
            20,
            0.5,
            0.85).dataSync();
        var boxes = [];
        var idx;
        for (let i = 0; i < indices.length; i++) {
            idx = indices[i];
            var score = scores.get(0, idx, 0);
            // Original order is [minY, minX, maxY, maxX] so we reorder.
            var box = {
                upperLeft: [tboxes.get(0, idx, 0, 1), tboxes.get(0, idx, 0, 0)],
                lowerRight: [tboxes.get(0, idx, 0, 3), tboxes.get(0, idx, 0, 2)]
            };
            boxes.push({score: score, box: box});
        }
        return {boxes: boxes, timestamp: Date.now()};
    });
    scores.dispose();
    tboxes.dispose();
    if (handsDetected.boxes.length !== 1) {
        if (handInterface.handDetectionRunning) {
            window.requestAnimationFrame(detectHands);
        }
        return;
    }
    const box = handsDetected.boxes[0].box;
    const [x1, y1] = box.upperLeft;
    const [x2, y2] = box.lowerRight;
    const area = {area: (x2 - x1) * (y2 - y1), timestamp: Date.now()};
    handInterface.handDetectionAreas = [].concat(handInterface.handDetectionAreas.slice(-2), [area]);
    // Update hand location
    const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
    const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
    const p = [0.5 * (x1 + x2) * vw, 0.5 * (y1 + y2) * vh];
    handInterface.positions = [].concat(handInterface.positions.slice(-2), [p]);
    var target = handInterface.targetElement;
    if (target === null) {
        target = handInterface.leninHand;
    }
    // Compute average position
    const [p1, p2, p3] = handInterface.positions;
    const [ap1, ap2] = [(1/3)*(p1[0] + p2[0] + p3[0]), (1/3)*(p1[1] + p2[1] + p3[1])];
    target.partProperties.setPropertyNamed(target, "left", ap1);
    target.partProperties.setPropertyNamed(target, "top", ap2);
    // Extract area information without any timestamps
    var justAreas = [];
    for (var i = 0; i < handInterface.handDetectionAreas.length; ++i) {
        justAreas.push(handInterface.handDetectionAreas[i].area);
    }
    var justAreas = [].concat(Array(3 - justAreas.length).fill(0), justAreas);
    // Check if hand is pushing in
    const [a1, a2, a3] = justAreas;
    const aveArea = (1/3) * (a1 + a2 + a3);
    if (aveArea > 0.25) {
        if (!handInterface.handMasked) {
            handInterface.handMasked = true;
            setTimeout(() => { handInterface.handMasked = false; }, 3000);
            if (handInterface.targetElement === null) {
                let closestView = findClosestView([p1, p2]);
                if (closestView !== null) {
                    handInterface.leninHand.partProperties.setPropertyNamed(handInterface.leninHand, "hide", true);
                    handInterface.targetElement = closestView.model;
                }
            } else {
                handInterface.leninHand.partProperties.setPropertyNamed(handInterface.leninHand, "hide", false);
                handInterface.targetElement = null;
            }
        }
    }
    if (handInterface.handDetectionRunning) {
        window.requestAnimationFrame(detectHands);
    }
};

const findClosestView = (point) => {
    let views = [];
    window.System.getCurrentCardModel().subparts.forEach((part) => {
        let partViews = window.System.findViewsById(part.id);
        partViews.forEach((view) => {
            views.push(view);
        })
    });
    var [closestDist, closestView] = [Infinity, null];
    views.forEach((view) => {
        let viewDist = dist(point, getVertices(view));
        if (viewDist < closestDist) {
            closestDist = viewDist;
            closestView = view;
        }
    });
    return closestView;
}

// https://aaronsmith.online/easily-load-an-external-script-using-javascript/
const loadScript = src => {
    return new Promise((resolve, reject) => {
        if (typeof window.tf !== 'undefined') {
            console.log("tensorflowjs already loaded");
            resolve();
            return;
        }
        console.log("loading tensorflowjs");
        const script = document.createElement('script');
        script.type = 'text/javascript';
        script.onload = resolve;
        script.onerror = reject;
        script.src = src;
        document.head.append(script);
    });
};

const loadHandDetectionModel = () => {
    handInterface.handDetectionAreas = [];
    loadScript("https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.13.5/dist/tf.js").then(() => {
        window.tf.loadFrozenModel(
            "https://cdn.jsdelivr.net/npm/handtrackjs/models/web/ssdlitemobilenetv2/tensorflowjs_model.pb",
            "https://cdn.jsdelivr.net/npm/handtrackjs/models/web/ssdlitemobilenetv2/weights_manifest.json"
        ).then(model => {
            console.log("hand detection model loaded");
            handInterface.handDetectionModel = model;
        }).then(() => {
            return navigator.mediaDevices.getUserMedia({ video: true });
        }).then(stream => {
            video.srcObject = stream;
            return video.play();
        }).then(() => {
            console.log("video started");
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            ctx.setTransform(-1, 0, 0, 1, canvas.width, 0); // Mirror incoming video
            handInterface.handDetectionRunning = true;
            handInterface.leninHand = window.System.newModel('image', window.System.getWorldStackModel().id, "/images/leninHand.png");
            handInterface.targetElement = null;
            window.requestAnimationFrame(detectHands);
        }).catch(err => {
            console.log("error loading hand detection model");
            console.log(err);
        });
    });
}

const unloadHandDetectionModel = () => {
    handInterface.handDetectionRunning = false;
    window.System.deleteModel(handInterface.leninHand.id)
    handInterface.leninHand = null;
    video.pause();
    const tracks = video.srcObject.getTracks();
    for (var i = 0; i < tracks.length; i++) {
        tracks[i].stop();
    }
    video.srcObject = null;
    console.log("video stopped");
    handInterface.handDetectionModel = null;
    console.log("unloading hand detection model");
}

const getVertices = (element) => {
    const rect = element.getBoundingClientRect();
    const upperLeft = [rect.x, rect.y];
    const upperRight = [rect.x + rect.width, rect.y];
    const lowerLeft = [rect.x, rect.y + rect.height];
    const lowerRight = [rect.x + rect.width, rect.y + rect.height];
    return {
        upperLeft: upperLeft,
        upperRight: upperRight,
        lowerLeft: lowerLeft,
        lowerRight: lowerRight
    };
}

const dist = (point, vertices) => {
    const [p1, p2] = point;
    const [ul1, ul2] = vertices.upperLeft;
    const [ll1, ll2] = vertices.lowerLeft;
    const [ur1, ur2] = vertices.upperRight;
    const [lr1, lr2] = vertices.lowerRight;
    // First check if the point is inside the rectangle
    // Next we compute the vector pointing from the point to the closest point
    // on the rectangle. There are 9 cases. The first is when the poinst is
    // inside the rectangle. The next four cases are if the point in one of
    // the four corners and the final four cases are when the point is on one
    // of the four sides.
    var [v1, v2] = [null, null];
    if ((ul1 <= p1) && (p1 <= lr1) && (ul2 <= p2) && (p2 <= lr2)) {
        // Case 0: inside the rectangle
        [v1, v2] = [0, 0];
    } else if ((p1 <= ul1) && (p2 <= ul2)) {
        // Case 1: upper left
        [v1, v2] = [ul1 - p1, ul2 - p2];
    } else if ((p1 >= ur1) && (p2 <= ur2)) {
        // Case 2: upper right
        [v1, v2] = [ur1 - p1, ur2 - p2];
    } else if ((p1 <= ll1) && (p2 >= ll2)) {
        // Case 3: lower left
        [v1, v2] = [ll1 - p1, ll2 - p2];
    } else if ((p1 >= lr1) && (p2 >= lr2)) {
        // Case 4: lower right
        [v1, v2] = [lr1 - p1, lr2 - p2];
    } else if (p1 <= ul1) {
        // Case 5: side left
        [v1, v2] = [ul1 - p1, 0];
    } else if (p1 >= lr1) {
        // Case 6: side right
        [v1, v2] = [lr1 - p1, 0];
    } else if (p2 <= ul2) {
        // Case 7: side top
        [v1, v2] = [0, ul2 - p2];
    } else if (p2 >= lr2) {
        // Case 8: side bottom
        [v1, v2] = [0, lr2 - p2];
    } else {
        // Case 9: inside
        [v1, v2] = [0, 0];
    }
    return Math.sqrt(v1*v1 + v2*v2);
}

class HandInterface {
    constructor() {
        this.handDetectionModel = null;
        this.handDetectionRunning = false;
        this.leninHand = null;
        this.handMasked = false;
        this.targetElement = null;
        this.handDetectionAreas = [];
        this.positions = [[0, 0], [0, 0], [0, 0]];
        // XXX - Only here to ignore the tensorflow warnings
        console.warn = () => {};
    }

    start() {
        loadHandDetectionModel();
    }

    stop() {
        unloadHandDetectionModel();
    }
}

const Testables = {
    dist: dist
}

const handInterface = new HandInterface();

export {
    Testables,
    handInterface,
    handInterface as default
};


<|start_filename|>js/objects/tests/test-parts-with-preload.js<|end_filename|>
/**
 * General Part Tests 
 * -------------------------------------------
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

let currentCard;
let button;
describe('Basic functionality', () => {
    describe('Setup', () => {
        it('Can add a new button to the current card', () => {
            let makeButtonFunction = function(){
                currentCard = System.getCurrentCardModel();
                let msg = {
                    type: 'command',
                    commandName: 'newModel',
                    args: ['button', currentCard.id]
                };
                currentCard.sendMessage(msg, currentCard);
            };
            expect(makeButtonFunction).to.not.throw();
        });
        it('Can find the button object, which has NO command handlers yet', () => {
            button = System.getCurrentCardModel().subparts.find(subpart => {
                return subpart.type == 'button';
            });
            assert.exists(button);
            assert.isEmpty(button._commandHandlers);
        });
    });
    describe('Core methods"', () => {
        it('Can get owner branch', () => {
            let branch = button.getOwnerBranch();
            let branchNodeByType = branch.map((node) => {return node.type;});
            assert.deepEqual(branchNodeByType, ["button", "card", "stack", "world", undefined]);
            assert.equal(branch[0].id, button.id);
        });
    });
    describe('Command Handlers"', () => {
        it('Can properly list all boot up command handlers present', () => {
            // we just check for a few of the core handlers and make sure these
            // are present, as these evolve too quickly to keep a comprehensive list
            assert.deepEqual(button.commandHandlerRegistry["answer"], {partId: -1, partType: "System", override: false, private: false});
        });
        it('Adding a new command handler adds it to the registry', () => {
            button._commandHandlers["newHandler"] = function(){};
            assert.deepEqual(button.commandHandlerRegistry["newHandler"], {partId: button.id, partType: "button", override: false, private: false});
        });
        it('Overriding a command handler is properly registered', () => {
            button._commandHandlers["answer"] = function(){};
            assert.deepEqual(button.commandHandlerRegistry["answer"], {partId: button.id, partType: "button", override: true, private: false});
        });
    });
});


<|start_filename|>js/objects/views/drawing/EraserTool.js<|end_filename|>
/**
 * EraserTool
 * -----------------------------------
 * I provide eraser-like functionality on
 * my parent element's shadow canvas.
 * I am specifically designed for use as a
 * child of DrawingView
 */
const eraserSVG = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-eraser" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M19 19h-11l-4 -4a1 1 0 0 1 0 -1.41l10 -10a1 1 0 0 1 1.41 0l5 5a1 1 0 0 1 0 1.41l-9 9" />
  <line x1="18" y1="12.3" x2="11.7" y2="6" />
</svg>
`;

const eraserToolTemplateString = `
<style>
    :host {
        display: flex;
        position: relative;
        margin-bottom: 6px;
    }
    #tool-button {
        --active-color: black;
        --inactive-color: rgb(170, 170, 170);
        --hover-color: rgb(140, 140, 140);
        display: block;
        width: 24px;
        height: 24px;
        border-width: 1px;
        border-style: solid;
        border-color: var(--inactive-color);
        color: var(--inactive-color);
    }
    :host([active="true"]) > #tool-button {
        border-color: var(--active-color);
        color: var(--active-color);
    }
    #brushes-container {
        position: relative;
        margin-left: 6px;
        display: none;
    }
    #brush-adjuster {
        position: relative;
        display: flex;
        margin-left: 6px;
        box-sizing: border-box;
        border-width: 1px;
        border-style: solid;
        border-color: var(--active-color);
    }
    #brush-slider,
    #brush-number {
        box-sizing: border-box;
    }
    #brush-number {
        max-width: 3rem;
    }
    :host([active="true"]) > #brushes-container {
        display: flex;
    }
</style>
<div id="tool-button">
  ${eraserSVG}
</div>
<div id="brushes-container">
  <div id="brush-adjuster">
    <input id="brush-slider" type="range" min="1" max="100" step="1">
    <input id="brush-number" type="number">
  </div>
</div>
`;

class EraserTool extends HTMLElement {
    constructor(){
        super();

        // Setup shadow dom. This tool will
        // display itself as a button that can
        // be toggled.
        this.template = document.createElement('template');
        this.template.innerHTML = eraserToolTemplateString;
        this.shadow = this.attachShadow({mode: 'open'});
        this.shadow.append(
            this.template.content.cloneNode(true)
        );

        // Default drawing context is null.
        // This will be set if and when this tool is
        // connected to a parent element that has a context
        this.ctx = null;

        // Bind component methods
        this.start = this.start.bind(this);
        this.end = this.end.bind(this);
        this.onMove = this.onMove.bind(this);
        this.toggleActive = this.toggleActive.bind(this);
        this.setContextFromAttributes = this.setContextFromAttributes.bind(this);
        this.handleBrushSliderChange = this.handleBrushSliderChange.bind(this);
        this.handleBrushNumberInputChange = this.handleBrushNumberInputChange.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.setAttribute('role', 'tool');
            this.setAttribute('active', false);
            if(!this.hasAttribute('width')){
                this.setAttribute('width', '6');
            }
            if(this.parentElement.drawingContext){
                this.ctx = this.parentElement.drawingContext;

                // If I am the only tool in my parent,
                // set myself to active
                let siblingTools = this.parentElement.querySelectorAll('[role="tool"]');
                if(siblingTools.length == 1){
                    this.setAttribute('active', true);
                }
            }

            // Attach event listeners
            this.button = this.shadowRoot.getElementById('tool-button');
            this.button.addEventListener('click', this.toggleActive);
            this.brushSlider = this.shadowRoot.getElementById('brush-slider');
            this.brushSlider.addEventListener('input', this.handleBrushSliderChange);
            this.brushNumberInput = this.shadowRoot.getElementById('brush-number');
            this.brushNumberInput.addEventListener('input', this.handleBrushNumberInputChange);

            // If there are is currently a width set,
            // update the slider and number input accordingly
            let currentWidth = this.getAttribute('width');
            if(currentWidth){
                this.brushSlider.value = parseInt(currentWidth);
                this.brushNumberInput.value = parseInt(currentWidth);
            }
        }
    }

    disconnectedCallback(){
        this.ctx = null;
        this.button.removeEventListener('click', this.toggleActive);
        this.brushSlider.removeEventListener('input', this.handleBrushSliderChange);
        this.brushNumberInput.removeEventListener('input', this.handleBrushNumberInputChange);
    }

    start(x, y){
        this.setContextFromAttributes();
        this.cachedStrokeStyle = this.ctx.strokeStyle;
        this.ctx.strokeStyle = 'red';
        this.ctx.globalCompositeOperation = 'destination-out';
        this.ctx.moveTo(x, y);
        this.ctx.beginPath();
    }

    onMove(x, y){
        this.ctx.lineTo(x, y);
        this.ctx.stroke();
    }

    end(x, y){
        this.ctx.lineTo(x, y);
        this.ctx.stroke();
        this.ctx.closePath();
        this.ctx.globalCompositeOperation = 'source-over';
        this.ctx.strokeStyle = this.cachedStrokeStyle;
    }

    setContextFromAttributes(){
        // line cap
        let lineCap = this.getAttribute('line-cap');
        if(lineCap){
            this.ctx.lineCap = lineCap;
        } else {
            this.ctx.lineCap = "round"; // default
        }

        // line join
        let lineJoin = this.getAttribute('line-join');
        if(lineJoin){
            this.ctx.lineJoin = lineJoin;
        } else {
            this.ctx.lineJoin = "round";
        }

        // line width
        let lineWidth = this.getAttribute('width');
        if(lineWidth){
            this.ctx.lineWidth = parseInt(lineWidth);
        } else {
            this.ctx.lineWidth = 6;
        }
    }

    static get observedAttributes(){
        return [
            'width',
            'line-join',
            'line-cap'
        ];
    }

    attributeChangedCallback(name, oldVal, newVal){
        if(name == 'width'){
            if(this.brushSlider){
                this.brushSlider.value = parseInt(newVal);
            }
            if(this.brushNumberInput){
                this.brushNumberInput.value = parseInt(newVal);
            }
        }
    }

    handleBrushSliderChange(event){
        this.setAttribute('width', event.target.value);
    }

    handleBrushNumberInputChange(event){
        this.setAttribute('width', event.target.value);
    }

    toggleActive(event){
        let isActive = this.getAttribute('active');
        if(isActive == "true"){
            this.setAttribute('active', 'false');
        } else {
            // First, find any other tools in my parent
            // element and deactivate them.
            Array.from(this.parentElement.querySelectorAll('[role="tool"]'))
                .filter(el => {
                    return el.getAttribute('active') == 'true';
                })
                .forEach(el => {
                    el.setAttribute('active', 'false');
                });

            // Set this tool to be active
            this.setAttribute('active', 'true');
        }
    }
};
window.customElements.define('eraser-tool', EraserTool);

export {
    EraserTool,
    EraserTool as default
};


<|start_filename|>js/objects/parts/Area.js<|end_filename|>
/**
 * Area
 * ----------------------------------
 * I am a Area Part.
 * I represent a 'grouping' of subparts within
 * my owner part.
 * I contain the Layout properties set, and therefore
 * can display my contained subparts according to 
 * different layout properties than my ancestor
 * Card.
 *
 */
import {Part} from './Part.js';
import {
    addBasicStyleProps,
    addPositioningStyleProps,
    addLayoutStyleProps
} from '../utils/styleProperties.js';

const sides = ["top", "bottom", "left", "right"];

class Area extends Part {
    constructor(...args){
        super(...args);

        this.acceptedSubpartTypes = [
            "area",
            "button",
            "field",
            "image",
            "audio",
            "resource",
            "drawing",
            "browser",
            "window"
        ];

        // Add style props
        addBasicStyleProps(this);
        addPositioningStyleProps(this);
        addLayoutStyleProps(this);

        // Set default width and height
        // for an empty area
        this.partProperties.setPropertyNamed(
            this,
            'width',
            50,
            true, // notify
            true // set default
        );
        this.partProperties.setPropertyNamed(
            this,
            'height',
            50,
            true, // notify
            true // set default
        );
        this.partProperties.newBasicProp(
            'clipping',
            false
        );
        this.partProperties.newBasicProp(
            'allow-scrolling',
            false
        );
        this.setupStyleProperties();
        // part specific default style properties
        this.partProperties.setPropertyNamed(
            this,
            'background-transparency',
            0,
            true, // notify
            true // set default
        );
        sides.forEach((s) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${s}-width`,
                1,
                true, // notify
                true // set default
            );
        });
        sides.forEach((s) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${s}-color`,
                "rgb(238, 238, 238)",
                true, // notify
                true // set default
            );
        });
        sides.forEach((s) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${s}-transparency`,
                0.5,
                true, // notify
                true // set default
            );
        });
    }


    get type(){
        return 'area';
    }
};

export {
    Area,
    Area as default
};


<|start_filename|>js/objects/tests/test-property-value-with-preload.js<|end_filename|>
/**
 * PropertyValue semantic rule tests
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

let currentCardModel;
let buttonModel;
let drawingModel;

function compileButtonScript(aScript){
    let msg = {
        type: 'compile',
        codeString: aScript,
        targetId: buttonModel.id
    };
    window.System.compile(msg);
}

describe("PropertyValue Interpreter Tests", () => {
    describe('Model Setup', () => {
        it('Can find the current card model', () => {
            let currentCardView = document.querySelector('.current-stack > .current-card');
            currentCardModel = currentCardView.model;
            assert.exists(currentCardModel);
        });
        it('Can add a button to current card model without error', () => {
            let addButton = function(){
                let msg = {
                    type: 'command',
                    commandName: 'newModel',
                    args: [
                        'button',
                        currentCardModel.id,
                        'card'
                    ]
                };
                currentCardModel.sendMessage(msg, currentCardModel);
            };
            expect(addButton).to.not.throw(Error);
        });
        it('Can find newly created button model', () => {
            let button = currentCardModel.subparts.filter(subpart => {
                return subpart.type == 'button';
            })[0];
            buttonModel = button;
            assert.exists(buttonModel);
        });
        it('Can add a drawing to current card model without error', () => {
            let addDrawing = function(){
                let msg = {
                    type: 'command',
                    commandName: 'newModel',
                    args: [
                        'drawing',
                        currentCardModel.id,
                        'card'
                    ]
                };
                currentCardModel.sendMessage(msg, currentCardModel);
            };
            expect(addDrawing).to.not.throw(Error);
        });
        it('Can find newly created drawing model', () => {
            let drawing = currentCardModel.subparts.filter(subpart => {
                return subpart.type == 'drawing';
            })[0];
            drawingModel = drawing;
            assert.exists(drawingModel);
        });
    });
    describe('Adding and deleting properties', () => {
        it('Can add a property to button (with object specifier) without error', () => {
            let addProperty = function(){
                let msg = {
                    type: 'command',
                    commandName: 'newProperty',
                    args: [
                        'test-prop',
                        buttonModel.id,
                    ]
                };
                currentCardModel.sendMessage(msg, buttonModel);
            };
            expect(addProperty).to.not.throw(Error);
            assert.isNotNull(buttonModel.partProperties.findPropertyNamed("test-prop"));
        });
        it('Can delete a property to button (with object specifier) without error', () => {
            let deleteProperty = function(){
                let msg = {
                    type: 'command',
                    commandName: 'deleteProperty',
                    args: [
                        'test-prop',
                        buttonModel.id,
                    ]
                };
                currentCardModel.sendMessage(msg, buttonModel);
            };
            expect(deleteProperty).to.not.throw(Error);
            assert.isNull(buttonModel.partProperties.findPropertyNamed("test-prop"));
        });
        it('Can add a property to button (in context)) without error', () => {
            let addProperty = function(){
                let msg = {
                    type: 'command',
                    commandName: 'newProperty',
                    args: [
                        'test-prop',
                    ]
                };
                buttonModel.sendMessage(msg, buttonModel);
            };
            expect(addProperty).to.not.throw(Error);
            assert.isNotNull(buttonModel.partProperties.findPropertyNamed("test-prop"));
        });
        it('Can delete a property to button (in context) without error', () => {
            let deleteProperty = function(){
                let msg = {
                    type: 'command',
                    commandName: 'deleteProperty',
                    args: [
                        'test-prop',
                    ]
                };
                buttonModel.sendMessage(msg, buttonModel);
            };
            expect(deleteProperty).to.not.throw(Error);
            assert.isNull(buttonModel.partProperties.findPropertyNamed("test-prop"));
        });
    });
    describe("PropertyValue lookup tests", () => {
        before(() => {
            currentCardModel.partProperties.setPropertyNamed(
                currentCardModel,
                'name',
                'TEST_CARD'
            );
            buttonModel.partProperties.setPropertyNamed(
                buttonModel,
                'name',
                'TEST_BUTTON'
            );
        });

        it("Button can read implicit property value on itself", () => {
            let script = [
                'on click',
                'put the "name" into global result',
                'end click'
            ].join('\n');
            compileButtonScript(script);
            buttonModel.sendMessage({type:'command', commandName:'click', args:[]}, buttonModel);
            let result = window.System.executionStack.getGlobal('result');
            assert.equal(result, 'TEST_BUTTON');
        });
        it("Button can read prop value using 'this button'", () => {
            let script = [
                'on click',
                'put the "name" of this button into global result',
                'end click'
            ].join('\n');
            compileButtonScript(script);
            buttonModel.sendMessage({type:'command', commandName:'click', args:[]}, buttonModel);
            let result = window.System.executionStack.getGlobal('result');
            assert.equal(result, 'TEST_BUTTON');
        });
        it("Button should error when trying to read prop value using 'this field' (wrong part type)", () => {
            let script = [
                'on click',
                'put the "name" of this field into global result',
                'end click'
            ].join('\n');
            compileButtonScript(script);
            let sendFunction = function(){
                buttonModel.sendMessage({type:'command', commandName:'click', args:[]}, buttonModel);
            };
            expect(sendFunction).to.throw(Error);
        });
        it("Button should be able to get prop of parent card via specifier", () => {
            let script = [
                'on click',
                'put the "name" of first card of current stack into global result',
                'end click'
            ].join('\n');
            compileButtonScript(script);
            buttonModel.sendMessage({type:'command', commandName:'click', args:[]}, buttonModel);
            let result = window.System.executionStack.getGlobal('result');
            assert.equal(result, 'TEST_CARD');
        });
        it("Button should be able to get prop of itself via long specifier", () => {
            let script = [
                'on click',
                'put the "name" of first button of card 1 of current stack into global result',
                'end click'
            ].join('\n');
            compileButtonScript(script);
            buttonModel.sendMessage({type:'command', commandName:'click', args:[]}, buttonModel);
            let result = window.System.executionStack.getGlobal('result');
            assert.equal(result, 'TEST_BUTTON');
        });
    });
    describe("Dynamic Property Tests", () => {
        it("Parts have the expected 'number' property", () => {
            let result = buttonModel.partProperties.getPropertyNamed(buttonModel, 'number');
            assert.equal(1, result);
            result = drawingModel.partProperties.getPropertyNamed(drawingModel, 'number');
            assert.equal(2, result);
        });
    });
});


<|start_filename|>js/objects/tests/test-hand-interface-with-preload.js<|end_filename|>
import chai from 'chai';

import {
    Testables
} from '../utils/handInterface.js';

describe("Hand Interface Tests", () => {
    const vertices = {
        upperLeft: [1, 1],
        lowerLeft: [1, 2],
        upperRight: [2, 1],
        lowerRight: [2, 2]
    };

    it("Distance Function: Upper Left", () => {
        const point = [0, 0];
        chai.assert.equal(Testables.dist(point, vertices), Math.sqrt(1*1 + 1*1));
    });
    it("Distance Function: Upper Right", () => {
        const point = [4, 0];
        chai.assert.equal(Testables.dist(point, vertices), Math.sqrt(2*2 + 1*1));
    });
    it("Distance Function: Lower Left", () => {
        const point = [0, 5];
        chai.assert.equal(Testables.dist(point, vertices), Math.sqrt(1*1 + 3*3));
    });
    it("Distance Function: Lower Right", () => {
        const point = [5, 5];
        chai.assert.equal(Testables.dist(point, vertices), Math.sqrt(3*3 + 3*3));
    });

    it("Distance Function: Left Side", () => {
        const point = [0, 1];
        chai.assert.equal(Testables.dist(point, vertices), 1-0);
    });
    it("Distance Function: Right Side", () => {
        const point = [5, 1];
        chai.assert.equal(Testables.dist(point, vertices), 5-2);
    });
    it("Distance Function: Top Side", () => {
        const point = [2, 0];
        chai.assert.equal(Testables.dist(point, vertices), 2-1);
    });
    it("Distance Function: Bottom Side", () => {
        const point = [2, 4];
        chai.assert.equal(Testables.dist(point, vertices), 4-2);
    });

    it("Distance Function: Upper Left Vertex", () => {
        chai.assert.equal(Testables.dist(vertices.upperLeft, vertices), 0);
    });
    it("Distance Function: Upper Right Vertex", () => {
        chai.assert.equal(Testables.dist(vertices.upperRight, vertices), 0);
    });
    it("Distance Function: Lower Left Vertex", () => {
        chai.assert.equal(Testables.dist(vertices.lowerLeft, vertices), 0);
    });
    it("Distance Function: Lower Right Vertex", () => {
        chai.assert.equal(Testables.dist(vertices.lowerRight, vertices), 0);
    });

    it("Distance Function: On Left Side", () => {
        const point = [1, 1.5];
        chai.assert.equal(Testables.dist(point, vertices), 0);
    });
    it("Distance Function: On Right Side", () => {
        const point = [2, 1.5];
        chai.assert.equal(Testables.dist(point, vertices), 0);
    });
    it("Distance Function: On Top Side", () => {
        const point = [1.5, 1];
        chai.assert.equal(Testables.dist(point, vertices), 0);
    });
    it("Distance Function: On Bottom Side", () => {
        const point = [1.5, 2];
        chai.assert.equal(Testables.dist(point, vertices), 0);
    });

    it("Distance Function: Inside", () => {
        const point = [1.5, 1.5];
        chai.assert.equal(Testables.dist(point, vertices), 0);
    });

    it("Distance Function: Scaled", () => {
        const point = [0, 0];
        const scaledVertices = {
            upperLeft: [10, 10],
            lowerLeft: [10, 20],
            upperRight: [20, 10],
            lowerRight: [20, 20]
        };
        chai.assert.equal(Testables.dist(point, scaledVertices), Math.sqrt(10*10 + 10*10));
    });
});


<|start_filename|>js/objects/tests/test-field-preload.js<|end_filename|>
/**
 * Tests for the Field Part and View
 * -------------------------------------------
 * These are simple tests to make sure the Field
 * Part and corresponding View work correctly and together
 * as expected.
 * NOTE: These classes are designed to be replaced with
 * a more comprehensive Field part/view combo
 */
import chai from 'chai';
const assert = chai.assert;

import Field from '../parts/Field.js';
//import FieldView from '../views/FieldView.js';
const FieldView = System.availableViews['field'];

//window.customElements.define('st-field', FieldView);

let fieldModel;
describe('Field Part/Model Tests', () =>{
    it('Can initialize the part', () => {
        fieldModel = new Field();
        'AAAAA'.replace("A", "B");
        assert.exists(fieldModel);
        assert.equal(fieldModel.type, 'field');
    });
});

let fieldView;
describe('FieldView basic tests', () => {
    before('', () => {
        let text = "some text";
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'text',
            text
        );
        let html = "<div>some text</div>";
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'innerHTML',
            html
        );
    });
    it('Can create a view element', () => {
        fieldView = document.createElement('st-field');
        assert.exists(fieldView);
    });
    it('Can set the view model', () => {
        fieldView.setModel(fieldModel);
        assert.equal(fieldView.model, fieldModel);
    });
    it('Has valid shadowDOM textarea', () => {
        let textArea = fieldView._shadowRoot.querySelector('.field-textarea');
        assert.exists(textArea);
    });
    it('Can connect to the DOM without issue', () => {
        document.body.append(fieldView);
        let found = document.querySelector('st-field');
        assert.equal(fieldView, found);
    });
    it('Mounted shadow textarea has current model text value', () => {
        let textArea = fieldView._shadowRoot.querySelector('.field-textarea');
        let modelValue = fieldModel.partProperties.getPropertyNamed(
            fieldModel,
            'text'
        );
        assert.equal(textArea.textContent, modelValue);
    });
    it('innerHTML is updated after text is', () => {
        let textArea = fieldView._shadowRoot.querySelector('.field-textarea');
        let newText = `on message\n   something NEW\nend message`;
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'text',
            newText
        );
        let htmlValue = fieldModel.partProperties.getPropertyNamed(
            fieldModel,
            'innerHTML'
        );

        assert.equal(htmlValue, newText);
    });
    it('textToHtml and htmlToText are idempotent 1 (empty)', () => {
        let fieldView = document.querySelector('st-field');
        let textContainer = document.createElement("div");
        let newContentHtml = "";
        textContainer.innerHTML = newContentHtml;

        let newContentText = newContentHtml;

        assert.equal(newContentText, fieldView.htmlToText(textContainer));
        // assert.equal(newContentHtml, fieldView.textToHtml(newContentText));
    });
    it('textToHtml and htmlToText are idempotent 2 (single line)', () => {
        let fieldView = document.querySelector('st-field');
        let textContainer = document.createElement("div");
        let newContentHtml = "I am a basic line";
        textContainer.innerHTML = newContentHtml;

        let newContentText = newContentHtml;

        assert.equal(newContentText, fieldView.htmlToText(textContainer));
        // assert.equal(newContentHtml, fieldView.textToHtml(newContentText));
    });
    it('textToHtml and htmlToText are idempotent 3 (with FF <br> tag)', () => {
        let fieldView = document.querySelector('st-field');
        let textContainer = document.createElement("div");
        let newContentHtml = "<div>on message</div><div>   some command</div><div>end message<br></div>";
        textContainer.innerHTML = newContentHtml;

        let newContentText = `on message\n   some command\nend message`;

        assert.equal(newContentText, fieldView.htmlToText(textContainer));
        // assert.equal(newContentHtml, fieldView.textToHtml(newContentText));
    });
    it('Entering text into the shadow textarea changes the model innerHTML and text prop', () => {
        let textArea = fieldView._shadowRoot.querySelector('.field-textarea');
        let newHTMLContent = "<div>on message</div><div>   something new</div><div>end message<br></div>";
        let newTextContent = `on message\n   something new\nend message`;

        // Simulate typing the input events
        let event = new window.Event('input');
        textArea.innerHTML = newHTMLContent;
        // due to some JSDOM weirdness we have to set the innerText explicitely, unlike in a 'real
        // DOM element '...ugggh
        textArea.innerText = newTextContent;
        textArea.dispatchEvent(event);

        let foundHTMLContent = fieldModel.partProperties.getPropertyNamed(
            fieldModel,
            'innerHTML'
        );
        assert.equal(newHTMLContent, foundHTMLContent);

        let foundTextContent = fieldModel.partProperties.getPropertyNamed(
            fieldModel,
            'text'
        );
        assert.equal(newTextContent, foundTextContent);
    });
});

class MockRange {
    constructor(){
        this.node = null;
    }

    selectNode(node){
        this.node = node;
    }

    insertNode(newNode){
        this.node.appendChild(newNode);
    }

    deleteContents(){
        this.node.innerHTML = '';
    }


}

describe('FieldView Target Range tests', () => {
    let range = null;
    let line1HTML = "<div>line1</div>";
    let line2HTML = "<div>line2</div>";
    before('Set up a range for testing', () => {
        let html = line1HTML + line2HTML;
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'innerHTML',
            html
        );
        assert.exists(fieldView);
        // NOTE: b/c of JSDON weirdness we need to update the innerText manually
        fieldView.textarea.innerText = "line1\nline2";
        // create a range and set it to the first child of the textarea
        let firstChild = fieldView.textarea.children[0];
        range = new MockRange();
        range.selectNode(firstChild);
        assert.equal(firstChild.innerHTML, range.node.innerHTML);
        // set the range as a selectedRange in the field view
        fieldView.selectionRanges['rangeUID'] = range;

    });
    it('Inserting Range (basic) updates the textarea', () => {
        let newHTML = "<div><span style='color: red'>new line </span></div>";
        // NOTE: b/c of JSDON weirdness we need to update the innerText manually
        fieldView.textarea.innerText = "new line\nline2";
        fieldView.insertRange('rangeUID', newHTML);
        let expectedHTML = '<div><span><div><span style="color: red">new line </span></div></span></div>' + line2HTML;
        assert.equal(expectedHTML, fieldView.textarea.innerHTML);
    });
    it('Inserting Range (basic) updates the "text" and "innerHTML" part properties', () => {
        let expectedHTML = '<div><span><div><span style="color: red">new line </span></div></span></div>' + line2HTML;
        let innerHTML = fieldModel.partProperties.getPropertyNamed(fieldModel, 'innerHTML');
        assert.equal(expectedHTML, innerHTML);
        let expectedText = "new line\nline2";
        let text = fieldModel.partProperties.getPropertyNamed(fieldModel, 'text');
        assert.equal(expectedText, text);
    });
    it.skip('Inserting Range (with target)', () => {
        // reset everything first
        let html = line1HTML + line2HTML;
        // NOTE: b/c of JSDON weirdness we need to update the innerText manually
        fieldView.textarea.innerText = "line1\nline2";
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'innerHTML',
            html
        );
        assert.equal(html, fieldView.textarea.innerHTML);
        assert.equal("line1\nline2", fieldModel.partProperties.getPropertyNamed(fieldModel, 'text'));
        // create a range and set it to the first child of the textarea
        let firstChild = fieldView.textarea.children[0];
        range = new MockRange();
        range.selectNode(firstChild);
        // set the range as a selectedRange in the field view
        fieldView.selectionRanges['rangeUID'] = range;

        // Note: here the fieldView will be its own target
        // set the range ID
        fieldModel.partProperties.setPropertyNamed(
            fieldModel,
            'targetRangeId',
            "rangeUID"
        );

        let newHTML = "<div><span style='color: red'>new line </span></div>";
        let target = `field id ${fieldModel.id}`;
        console.log(target);
        fieldView.setRangeInTarget(target, newHTML);
    });
});


<|start_filename|>js/objects/parts/Card.js<|end_filename|>
/**
 * Card
 * --------------------------
 * I am a Card Part.
 * I represent a collection of Parts that is
 * displayed on the screen. My owner is always
 * a Stack Part.
 * I can contain any kind of Part, including
 * buttons and fields.
 */
import {Part} from './Part.js';
import {
    BasicProperty
} from '../properties/PartProperties.js';

import {
    addBasicStyleProps,
    addLayoutStyleProps
} from '../utils/styleProperties.js';

class Card extends Part {
    constructor(owner, name){
        super(owner);
        this.stack = this._owner;
        this.acceptedSubpartTypes = [
            "window", "button",
            "field", "area", "drawing",
            "image", "audio", "browser", "resource"
        ];
        this.isCard = true;

        // Add Card-specific part
        // properties
        this.partProperties.newBasicProp(
            'marked',
            false
        );
        this.partProperties.newBasicProp(
            'cantDelete',
            false
        );
        this.partProperties.newBasicProp(
            'dontSearch',
            false
        );
        this.partProperties.newBasicProp(
            'showPict',
            false
        );

        // If we are initializing with a name
        // set the name property
        if(name){
            this.partProperties.setPropertyNamed(
                this,
                'name',
                name
            );
        }

        // Styling
        addBasicStyleProps(this);
        addLayoutStyleProps(this);
        this.setupStyleProperties();
        // part specific default style properties
        this.partProperties.setPropertyNamed(
            this,
            'background-color',
            "rgb(0, 75, 103)", // palette-blue
            true, // notify
            true  // set default
        );

        // remove the halo property and command handlers as these do not makes sense here
        let haloOpenProp = this.partProperties.findPropertyNamed("halo-open");
        if(haloOpenProp){
            this.partProperties.removeProperty(haloOpenProp);
        }
        this.removePrivateCommandHandler("openHalo");
        this.removePrivateCommandHandler("closeHalo");
    }

    get type(){
        return 'card';
    }
};

export {
    Card,
    Card as default
};


<|start_filename|>js/objects/views/AudioView.js<|end_filename|>
import PartView from './PartView.js';

const linkIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5" />
  <path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5" />
</svg>
`;

const templateString = `
<style>
:host {
    box-sizing: border-box;
    display: block;
    position: absolute;
    padding: 1px;
    user-select: none;
    cursor: pointer;
}

.wrapper{
    width: 100%;
    height: 100%;
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}
}
</style>
<div class="wrapper">
    <audio></audio>
    <span class="name"></span>
    <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-music" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
    <circle cx="6" cy="17" r="3"></circle>
    <circle cx="16" cy="17" r="3"></circle>
    <polyline points="9 17 9 4 19 4 19 17"></polyline>
    <line x1="9" y1="8" x2="19" y2="8"></line>
    </svg>
</div>
`;

// HTMLMediaElementStates copied from
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
const mediaStates = {
    0: "HAVE_NOTHING",
    1: "HAVE_METADATA",
    2: "HAVE_CURRENT_DATA",
    3: "HAVE_FUTURE_DATA",
    4: "HAVE_ENOUGH_DATA"
};

class AudioView extends PartView {
    constructor(){
        super();

        // Setup template and shadow dom
        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(this.template.content.cloneNode(true));

        // Bind component methods
        this.onClick = this.onClick.bind(this);
        this.initCustomHaloButton = this.initCustomHaloButton.bind(this);
        this.updateAudioLink = this.updateAudioLink.bind(this);
        this.play = this.play.bind(this);
        this.pause = this.pause.bind(this);
    }

    afterConnected(){
        let audio = this._shadowRoot.querySelector("audio");
        audio.addEventListener('loadeddata', () => {
            let stateCode = audio.readyState;
            this.model.partProperties.setPropertyNamed(
                this.model,
                "readyState",
                mediaStates[stateCode]
            );
        });

        if(!this.haloButton){
            this.initCustomHaloButton();
        }
    }

    afterDisconnected(){
    }

    afterModelSet(){
        let nameSpan = this._shadowRoot.querySelector(".name");
        nameSpan.innerText = this.model.partProperties.getPropertyNamed(this.model, "name");
        this.model.partProperties.setPropertyNamed(
            this.model,
            "readyState",
           "HAVE_NOTHING"
        );
        let audio = this._shadowRoot.querySelector("audio");
        let src = this.model.partProperties.getPropertyNamed(this.model, "src");
        if(src){
            audio.src = src;
        }
        // prop changes
        this.onPropChange("name", (value) => {
            nameSpan.innerText = value;
        });
        this.onPropChange("readyState", (value) => {
            let borderColor = "red";
            if(value == "HAVE_FUTURE_DATA" || value == "HAVE_ENOUGH_DATA"){
                borderColor = "green";
            };
            ["right", "left", "top", "bottom"].forEach((side) => {
                this.model.partProperties.setPropertyNamed(this.model, `border-${side}-color`, borderColor);
            });
        });
        this.onPropChange("play", (value) => {
            if(value === true){
                this.play();
            } else if (value === false){
                this.pause();
            }
        });
        this.onPropChange("stop", (value) => {
            if(value === true){
                audio.currentTime = 0;
            }
        });
        this.onPropChange("src", (url) => {
            try{
                // resource load is auto-loaded by the <audio> element
                audio.src = url;
            } catch(error){
                let errorMsg = {
                    type: "error",
                    name: "ResourceNotFound",
                    resourceType: "audio",
                    partId: this.model.id,
                    details: {source: url, type: "url"}

                };
                this.model.sendMessage({errorMsg}, this.model);
            }
        });
    }

    play(){
        // first make sure that the resource is ready
        let audio = this._shadowRoot.querySelector("audio");
        let readyState = this.model.partProperties.getPropertyNamed(this.model, "readyState");
        if(readyState == "HAVE_FUTURE_DATA" || readyState == "HAVE_ENOUGH_DATA"){
            audio.play();
        } else {
            alert(`audio is not ready; current state: ${readyState}`);
        }
    }

    pause(){
        this._shadowRoot.querySelector("audio").pause();
    }

    // re-loads the media, setting it back to the beggning
    stop(){
        this._shadowRoot.querySelector("audio").load();
    }

    onClick(event){
        if(event.button == 0){
            if(event.shiftKey){
                // prevent triggering the on click message
                event.preventDefault();
                if(this.hasOpenHalo){
                    this.model.partProperties.setPropertyNamed(this.model, "halo-open", false);
                } else {
                    this.model.partProperties.setPropertyNamed(this.model, "halo-open", true);
                }
            } else if(!this.hasOpenHalo){
                // Send the click command message to self
                this.model.sendMessage({
                    type: 'command',
                    commandName: 'click',
                    args: [],
                    shouldIgnore: true // Should ignore if System DNU
                }, this.model);
            }
        }
    }

    openHalo(){
        // Override default. Here we add a custom button
        // when showing.
        let foundHalo = this.shadowRoot.querySelector('st-halo');
        if(!foundHalo){
            foundHalo = document.createElement('st-halo');
            this.shadowRoot.appendChild(foundHalo);
        }
        foundHalo.append(this.haloButton);
    }

    initCustomHaloButton(){
        this.haloButton = document.createElement('div');
        this.haloButton.id = 'halo-audio-link';
        this.haloButton.classList.add('halo-button');
        this.haloButton.innerHTML = linkIcon;
        this.haloButton.style.marginTop = "6px";
        this.haloButton.setAttribute('slot', 'right-column');
        this.haloButton.setAttribute('title', 'Edit link for audio source');
        this.haloButton.addEventListener('click', this.updateAudioLink);
    }

    updateAudioLink(event){
        // Tells the model to update its
        // src link for the audio
        let currentSrc = this.model.partProperties.getPropertyNamed(
            this.model,
            'src'
        );
        let result = window.prompt("Edit URL for audio:", currentSrc);
        if(result && result !== '' && result !== currentSrc){
            this.sendMessage(
                {
                    type: 'command',
                    commandName: 'loadAudioFromSource',
                    args: [ result ]
                },
                this.model
            );
        }
    }


};

export {
    AudioView,
    AudioView as default
};


<|start_filename|>js/ohm/tests/test-grammar.js<|end_filename|>
ohm = require('ohm-js');
// Instantiate the grammar.
var fs = require('fs');
var g = ohm.grammar(fs.readFileSync('./js/ohm/simpletalk.ohm'));

var chai = require('chai');
var assert = chai.assert;

const matchTest = (str) => {
    const match = g.match(str);
    assert.isTrue(match.succeeded());
};
const semanticMatchTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};
const matchAndsemanticMatchTest = (str, semanticType) => {
    matchTest(str);
    const typeMatch = g.match(str, semanticType);
    assert.isTrue(typeMatch.succeeded());
};
const semanticMatchFailTest = (str, semanticType) => {
    const typeMatch = g.match(str, semanticType);
    assert.isFalse(typeMatch.succeeded());
};
String.prototype.unCapitalize = function() {
    return this.charAt(0).toLowerCase() + this.slice(1);
};

const objects = [
    "background", "card", "area", "field", "button", "stack", "window",
    "drawing", "audio", "image"
];

describe("SimpleTalk Grammar", () => {
    describe("Basic lexical", () => {
        it("`space` does not include any newline chars", () => {
            const newlineStr = `\n`;
            const crStr = `\r`;
            semanticMatchFailTest(newlineStr, 'space');
            semanticMatchFailTest(crStr, 'space');
        });
        it("simple comment", () => {
            const s = "--mycomment";
            matchAndsemanticMatchTest(s, 'comment');
        });
        it("simple comment with spaces", () => {
            const s = "-- mycomment part2";
            matchAndsemanticMatchTest(s, 'comment');
        });
        it("positive integer", () =>{
            const s = "24";
            semanticMatchTest(s, 'integerLiteral');
        });
        it("negative integer", () => {
            const s = "-24";
            semanticMatchTest(s, 'integerLiteral');
            semanticMatchTest(s, 'anyLiteral');
        });
        it("positive float", () => {
            const s = "0.002";
            semanticMatchTest(s, 'floatLiteral');
            semanticMatchTest(s, 'anyLiteral');
        });
        it("negative float", () => {
            const s = "-0.011";
            semanticMatchTest(s, 'floatLiteral');
            semanticMatchTest(s, 'anyLiteral');
        });
        it('variable name (correct)', () => {
            const s = "myCustomVariable";
            semanticMatchTest(s, 'variableName');
        });
        it('variable name (incorrect)', () => {
            const s = "my-custom-variable";
            semanticMatchFailTest(s, 'variableName');
        });
    });

    describe("Invalid overall scripts", () => {
        it("Cannot match a script with invalid handler open/close", () => {
            let script = [
                'on click',
                '\t myMessage',
                'end click',
                '\n',
                'on myMessage',
                '\tsnswer "hello"',
                'on myMessage'
            ].join('\n');
            let match = g.match(script);
            assert.isFalse(match.succeeded());
        });
        it("Fails to match on script with top-level invalid handler def", () => {
            let script = [
                'on doMyThing what',
                '\tanswer what',
                'on doMyThing',
                '\n',
                'on click',
                '\tdoMyThing "doing it"',
                'end click'
            ].join('\n');
            assert.isTrue(g.match(script).failed());
        });
    });

    // TODO add more tests for handlers, comments, end etc
    describe("Messages", () => {
        it ("Message name", () => {
            const strings = [
                "f", "F", "amessage", "aMessage", "aMessage"
            ];
            strings.forEach((s) => {
                semanticMatchTest(s, 'messageName');
            });
        });
        it ("Bad message name", () => {
            const strings = [
                "1", "1m", "1M", "m1", "M1", " message", "then", "on", "end"
            ];
            strings.forEach((s) => {
                semanticMatchFailTest(s, 'messageName');
            });
        });
        it ("Can parse message names beginning with 'do'", () => {
            let str = "doSomething";
            semanticMatchTest(str, 'messageName');
        });
        it ("Message handler (no args, no statements)", () => {
            const s = `on myMessage\nend myMessage`;
            matchAndsemanticMatchTest(s, 'MessageHandler');
        });
        it ("Message handler (args, no statements)", () => {
            const s = `on myNewMessage arg1, arg2\nend myNewMessage`;
            matchAndsemanticMatchTest(s, 'MessageHandler');
        });
        it('Does not match a message that has two handlerOpens', () => {
            let invalidStr = [
                'on myMessage',
                '\tsomeOtherMessage',
                'on myMessage'
            ].join('\n');
            let match = g.match(invalidStr);
            assert.isFalse(match.succeeded());
        });
        it ("Built in message syntax", () => {
            const strings = [
                "close", "closeStack", "commandKeyDown", "quit"
            ];
            strings.forEach((s) => {
                semanticMatchTest(s, 'Message');
                semanticMatchTest(s, 'Message_system');
            });
        });
        describe("Authored in message syntax", () => {
            it('Understands message with no arguments', () => {
                let str = "myNewMessage";
                semanticMatchTest(str, 'Message');
                semanticMatchTest(str, 'Message_authoredMessage');
            });
            it('Understands message with one (variableName) argument', () => {
                let str = "myNewMessage arg1";
                semanticMatchTest(str, 'Message');
                semanticMatchTest(str, 'Message_authoredMessage');
            });
            it('Understands message with two (variableName) arguments', () => {
                let str = "myNewMessage arg1, arg2";
                semanticMatchTest(str, "Message");
                semanticMatchTest(str, "Message_authoredMessage");
            });
            it('Understands message with three (variableName) arguments', () => {
                let str = "myNewMessage arg1, arg2, arg3";
                semanticMatchTest(str, 'Message');
                semanticMatchTest(str, 'Message_authoredMessage');
            });
            it('Underastands message with one (integer literal) argument', () => {
                let str = "myNewMessage 50";
                semanticMatchTest(str, 'Message');
                semanticMatchTest(str, 'Message_authoredMessage');
            });
        });
        describe("Built in system messages", () => {
            const tests = [
                {strings: [
                    "closeBackground", "closeButton", "closeCard", "closeField", "closeStack"
                ],
                name: "Close Object"},
                {strings: [
                    "openBackground", "openButton", "openCard", "openField", "openStack"
                ],
                name: "Open Object"},
                {strings: [
                    "deleteBackground", "deleteButton", "deleteCard", "deleteField", "deleteStack"
                ],
                name: "Delete Object"},
                {strings: [
                    "newBackground", "newButton", "newCard", "newField", "newStack"
                ],
                name: "New Object"},
                {strings: [
                    "mouseDoubleClick", "mouseDown", "mouseDownInPicture", "mouseEnter",
                    "mouseLeave", "mouseStillDown", "mouseUpInPicture", "mouseUp", "mouseWithin"
                ],
                name: "Mouse Event"},
                {strings: [
                    "arrowKey", "commandKeyDown", "controlKey", "enterKey",
                    "functionKey", "keyDown", "returnKey", "tabKey"
                ],
                skipSpecific: true,
                name: "Key Event"},
                {strings: [
                    "systemEvent", "doMenu", "enterInField" , "exitField", "help",
                    "hide menubar", "idle", "moveWindow", "quit", "resumeStack",
                    "resume", "returnInField", "show menubar", "sizeWindow", "startUp",
                    "suspendStack", "suspend"
                ],
                skipSpecific: true,
                name: "Other System Messages"},
            ];
            tests.forEach( (test) => {
                it(`${test.name}`, () => {
                    const cl = test.name.replace(" ", "").unCapitalize();
                    test.strings.forEach((s) => {
                        semanticMatchTest(s, 'Message');
                        semanticMatchTest(s, 'systemMessage');
                        if (!test.skipSpecific) {
                            semanticMatchTest(s, `systemMessage_${cl}`);
                        }
                    });
                });
            });
        });
    });
    describe("stringLiteral", () => {
        it('Can deal with a single word', () => {
            const s = '"test"';
            semanticMatchTest(s, 'stringLiteral');
        });
        it('Can deal with a sentence', () => {
            const s = '"this is a test"';
            semanticMatchTest(s, 'stringLiteral');
        });
        it('Can deal with a url', () => {
            const s = '"http://localhost:8000/js/objects/examples/bootstrap-example.html"';
            semanticMatchTest(s, 'stringLiteral');
        });
        it('Can deal with a path', () => {
            const s = '"../../examples/bootstrap-example.html"';
            semanticMatchTest(s, 'stringLiteral');
        });
        it('Can deal with whitespace', () => {
            const s = '" \t   hi \t  \s  "';
            semanticMatchTest(s, 'stringLiteral');
        });
        it('Can deal with characters that look like operators', () => {
            const s = `"hello ** this / is ^ a stringLiteral +"`;
            semanticMatchTest(s, 'stringLiteral');
        });
        it('Does not match if newline is present', () => {
            const s = '"this is a\t\ntest"';
            semanticMatchFailTest(s, 'stringLiteral');
        });
    });
    describe("punctuation", () => {
        it('Basic', () => {
            const strings = [":", ";", ".", ",", "?", "!", "-"];
            strings.forEach((s) => {semanticMatchTest(s, 'punctuation')});
        });
        it('Does not match some examples', () => {
            const s = '"this is a\t\ntest"';
            const strings = ["a", " ", "/"];
            strings.forEach((s) => {semanticMatchFailTest(s, 'punctuation')});
        });
    });
    describe("object Id", () => {
        it ("Basic Id", () => {
            semanticMatchTest("myNewId", "objectId");
        });
        it ("Id with constters and digits", () => {
            semanticMatchTest("newIdl123", "objectId");
        });
        it ("Bad objetId (with space)", () => {
            semanticMatchFailTest(" badId", "objectId");
        });
        it ("Bad objetId (with space)", () => {
            semanticMatchFailTest(" badId", "objectId");
        });
    });
    describe("Parameter List", () => {
        it ("Single param list", () => {
            semanticMatchTest("param1", "ParameterList");
        });
        it ("Simple param list", () => {
            semanticMatchTest("param1, param2", "ParameterList");
        });
        it ("Param list with digits", () => {
            semanticMatchTest("12, 22, newparam123", "ParameterList");
        });
        it ("Bad param list (without spaces)", () => {
            semanticMatchFailTest("param1,param2", "ParameterList");
        });
        it ("Bad param list (space)", () => {
            semanticMatchFailTest("pa ram1, param2", "ParameterList");
        });
    });
    describe("Variable assignment", () => {
        it("Can parse put assignment command (string literal)", () => {
            const s = `put "hello" into myCustomVariable`;
            semanticMatchTest(s, 'Command_putVariable');
        });
        it("Can parse put assignment command (integer literal)", () => {
            const s = `put 22 into myCustomVariable`;
            semanticMatchTest(s, 'Command_putVariable');
        });
        it("Can parse put assignment command (float literal)", () => {
            const s = `put -0.12 into myCustomVariable`;
            semanticMatchTest(s, 'Command_putVariable');
        });
        it("Can parse put assignment command into global variable (float literal)", () => {
            const s = `put -0.12 into global myCustomVariable`;
            semanticMatchTest(s, 'Command_putVariable');
        });
    });
});


<|start_filename|>js/objects/tests/test-private-commands-with-preload.js<|end_filename|>
/**
 * Testing the implementation of the
 * primitive add command in the interpreter
 */
import chai from 'chai';
import ohm from 'ohm-js';
const assert = chai.assert;
const expect = chai.expect;

import interpreterSemantics from '../../ohm/interpreter-semantics.js';
let testLanguageGrammar = ohm.grammar(window.grammar);

function getSemanticsFor(aPart){
    let semantics = testLanguageGrammar.createSemantics();
    semantics.addOperation(
        'interpret',
        interpreterSemantics(aPart, window.System)
    );
    return semantics;
}

describe('Private command tests', () => {
    let semantics;
    let currentStack;
    let currentCard;
    let button1;
    describe('Move', () => {
        before('', () => {
            currentCard = System.getCurrentCardModel();
            let initSemantics = function(){
                semantics = getSemanticsFor(currentCard);
            };
            expect(initSemantics).to.not.throw();
            let script = `add button "Button 1" to current card`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            assert.equal(currentCard.subparts.length, 1);
            button1 = currentCard.subparts[0];
            button1.partProperties.setPropertyNamed(button1, "wants-move", true);
        });
        it('Moving by x y', () => {
            let script = `tell button "Button 1" to move 10, 20`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            let top = button1.partProperties.getPropertyNamed(button1, "top");
            let left = button1.partProperties.getPropertyNamed(button1, "left");
            assert.equal(20, top);
            assert.equal(10, left);
        });
    });
    describe('Subpart re-order basic (no cards, stacks)', () => {
        let currentCardView;
        let button2;
        let button3;
        before('', () => {
            currentCard = System.getCurrentCardModel();
            currentCardView = System.findViewById(currentCard.id);
            let initSemantics = function(){
                semantics = getSemanticsFor(currentCard);
            };
            expect(initSemantics).to.not.throw();
            let script = `add button "Button 2" to current card`;
            let match = testLanguageGrammar.match(script, 'Command');
            let msg = semantics(match).interpret();
            currentCard.sendMessage(msg, currentCard);
            script = `add button "Button 3" to current card`;
            match = testLanguageGrammar.match(script, 'Command');
            msg = semantics(match).interpret();
            currentCard.sendMessage(msg, currentCard);
            button2 = currentCard.subparts[1];
            button3 = currentCard.subparts[2];
        });
        it('Check that subpart and DOM child order is in sync', () => {
            for(let i=0; i <=2; i++){
                let button = currentCard.subparts[i];
                let name = button.partProperties.getPropertyNamed(button, "name");
                assert.equal(`Button ${i+1}`, name);
                let buttonView = currentCardView.childNodes[i];
                assert.equal(button.id, buttonView.getAttribute("part-id"));
            }
        });
        it('MoveToLast', () => {
            let script = `tell button "Button 1" to moveToLast`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now last
            assert.equal(currentCard.subparts[2], button1);
            assert.equal(button1.id, currentCardView.childNodes[2].getAttribute("part-id"));
            // button 2 is now first
            assert.equal(currentCard.subparts[0], button2);
            assert.equal(button2.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 3 is now second
            assert.equal(currentCard.subparts[1], button3);
            assert.equal(button3.id, currentCardView.childNodes[1].getAttribute("part-id"));
        });
        it('MoveToLast is idempotent', () => {
            let script = `tell button "Button 1" to moveToLast`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now last
            assert.equal(currentCard.subparts[2], button1);
            assert.equal(button1.id, currentCardView.childNodes[2].getAttribute("part-id"));
            // button 2 is now first
            assert.equal(currentCard.subparts[0], button2);
            assert.equal(button2.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 3 is now second
            assert.equal(currentCard.subparts[1], button3);
            assert.equal(button3.id, currentCardView.childNodes[1].getAttribute("part-id"));
        });
        it('MoveToFirst', () => {
            let script = `tell button "Button 1" to moveToFirst`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now first 
            assert.equal(currentCard.subparts[0], button1);
            assert.equal(button1.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 2 is now second
            assert.equal(currentCard.subparts[1], button2);
            assert.equal(button2.id, currentCardView.childNodes[1].getAttribute("part-id"));
            // button 3 is now third
            assert.equal(currentCard.subparts[2], button3);
            assert.equal(button3.id, currentCardView.childNodes[2].getAttribute("part-id"));
        });
        it('MoveToFirst idempotent', () => {
            let script = `tell button "Button 1" to moveToFirst`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now first
            assert.equal(currentCard.subparts[0], button1);
            assert.equal(button1.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 2 is now second
            assert.equal(currentCard.subparts[1], button2);
            assert.equal(button2.id, currentCardView.childNodes[1].getAttribute("part-id"));
            // button 3 is now third
            assert.equal(currentCard.subparts[2], button3);
            assert.equal(button3.id, currentCardView.childNodes[2].getAttribute("part-id"));
        });
        it('MoveDown', () => {
            let script = `tell button "Button 1" to moveDown`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now second
            assert.equal(currentCard.subparts[1], button1);
            assert.equal(button1.id, currentCardView.childNodes[1].getAttribute("part-id"));
            // button 2 is now first
            assert.equal(currentCard.subparts[0], button2);
            assert.equal(button2.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 3 is now third
            assert.equal(currentCard.subparts[2], button3);
            assert.equal(button3.id, currentCardView.childNodes[2].getAttribute("part-id"));
        });
        it('MoveDown (again)', () => {
            let script = `tell button "Button 1" to moveDown`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now third
            assert.equal(currentCard.subparts[2], button1);
            assert.equal(button1.id, currentCardView.childNodes[2].getAttribute("part-id"));
            // button 2 is now first
            assert.equal(currentCard.subparts[0], button2);
            assert.equal(button2.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 3 is now second
            assert.equal(currentCard.subparts[1], button3);
            assert.equal(button3.id, currentCardView.childNodes[1].getAttribute("part-id"));
        });
        it('MoveDown (moving last part is idempotent)', () => {
            let script = `tell button "Button 1" to moveDown`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now third
            assert.equal(currentCard.subparts[2], button1);
            assert.equal(button1.id, currentCardView.childNodes[2].getAttribute("part-id"));
            // button 2 is now first
            assert.equal(currentCard.subparts[0], button2);
            assert.equal(button2.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 3 is now second
            assert.equal(currentCard.subparts[1], button3);
            assert.equal(button3.id, currentCardView.childNodes[1].getAttribute("part-id"));
        });
        it('MoveUp', () => {
            let script = `tell button "Button 1" to moveUp`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now second
            assert.equal(currentCard.subparts[1], button1);
            assert.equal(button1.id, currentCardView.childNodes[1].getAttribute("part-id"));
            // button 2 is now first
            assert.equal(currentCard.subparts[0], button2);
            assert.equal(button2.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 3 is now third
            assert.equal(currentCard.subparts[2], button3);
            assert.equal(button3.id, currentCardView.childNodes[2].getAttribute("part-id"));
        });
        it('MoveUp (again)', () => {
            let script = `tell button "Button 1" to moveUp`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now first
            assert.equal(currentCard.subparts[0], button1);
            assert.equal(button1.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 2 is now second
            assert.equal(currentCard.subparts[1], button2);
            assert.equal(button2.id, currentCardView.childNodes[1].getAttribute("part-id"));
            // button 3 is now third
            assert.equal(currentCard.subparts[2], button3);
            assert.equal(button3.id, currentCardView.childNodes[2].getAttribute("part-id"));
        });
        it('MoveUp (moving first part is idempotent)', () => {
            let script = `tell button "Button 1" to moveUp`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, button1);
            // button 1 is now first
            assert.equal(currentCard.subparts[0], button1);
            assert.equal(button1.id, currentCardView.childNodes[0].getAttribute("part-id"));
            // button 2 is now second
            assert.equal(currentCard.subparts[1], button2);
            assert.equal(button2.id, currentCardView.childNodes[1].getAttribute("part-id"));
            // button 3 is now third
            assert.equal(currentCard.subparts[2], button3);
            assert.equal(button3.id, currentCardView.childNodes[2].getAttribute("part-id"));
        });
    });
    describe('Subpart re-order with cards', () => {
        let currentStack;
        let currentStackView;
        let currentCardView;
        let  orderCheck = function(parentPart, newIndex, name){
            let parentView = System.findViewById(parentPart.id);
            let part = parentPart.subparts[newIndex];
            let nameProp = part.partProperties.getPropertyNamed(part, "name");
            assert.equal(nameProp, name);
            let view = System.findViewById(part.id);
            assert.equal(view, parentView.childNodes[newIndex]);
            assert.equal(part.id, view.getAttribute("part-id"));
        };
        before('', () => {
            currentCard = System.getCurrentCardModel();
            currentCard.partProperties.setPropertyNamed(currentCard, "name", "Current Card");
            currentCardView = System.findViewById(currentCard.id);
            currentStack = System.getCurrentStackModel();
            currentStackView = System.findViewById(currentStack.id);
            let initSemantics = function(){
                semantics = getSemanticsFor(currentCard);
            };
            expect(initSemantics).to.not.throw();
            let script = `add button "Button 1" to current stack`;
            let match = testLanguageGrammar.match(script, 'Command');
            let msg = semantics(match).interpret();
            currentCard.sendMessage(msg, currentCard);
            script = `add button "Button 2" to current stack`;
            match = testLanguageGrammar.match(script, 'Command');
            msg = semantics(match).interpret();
            currentCard.sendMessage(msg, currentCard);
            script = `add card "Card 2" to current stack`;
            match = testLanguageGrammar.match(script, 'Command');
            msg = semantics(match).interpret();
            currentCard.sendMessage(msg, currentCard);
            script = `add card "Card 3" to current stack`;
            match = testLanguageGrammar.match(script, 'Command');
            msg = semantics(match).interpret();
            currentCard.sendMessage(msg, currentCard);
        });
        it('Check that subpart and DOM child order is in sync', () => {
            // NOTE: cards precede buttons, even if the latter was added first
            // check order
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 1, "Card 2");
            orderCheck(currentStack, 2, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveToLast', () => {
            let script = `tell current card to moveToLast`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 0, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 2, "Current Card");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveToLast is idempotent', () => {
            let script = `tell current card to moveToLast`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 0, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 2, "Current Card");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
            script = `tell third card to moveToLast`;
            match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 0, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 2, "Current Card");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveToFirst', () => {
            let script = `tell current card to moveToFirst`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 1, "Card 2");
            orderCheck(currentStack, 2, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveToFirst is idempotent', () => {
            let script = `tell current card to moveToFirst`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 1, "Card 2");
            orderCheck(currentStack, 2, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveUp', () => {
            let script = `tell card "Card 2" to moveUp`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 1, "Current Card");
            orderCheck(currentStack, 0, "Card 2");
            orderCheck(currentStack, 2, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveDown', () => {
            let script = `tell card "Card 2" to moveDown`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 1, "Card 2");
            orderCheck(currentStack, 2, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveDown again', () => {
            let script = `tell card "Card 2" to moveDown`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 2, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveDown again (cannot move card after non-card part)', () => {
            let script = `tell card "Card 2" to moveDown`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            // check order
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 2, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveToFirst (cannot move non-card part before cards)', () => {
            let script = `tell button "Button 2" of current stack to moveToFirst`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 2, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 4, "Button 1");
            orderCheck(currentStack, 3, "Button 2");
        });
        it('MoveUp (can move non-card parts up before non-card parts)', () => {
            let script = `tell button "Button 1" of current stack to moveUp`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 2, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveUp (cannot move non-card part before cards)', () => {
            let script = `tell button "Button 1" of current stack to moveUp`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 2, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 3, "Button 1");
            orderCheck(currentStack, 4, "Button 2");
        });
        it('MoveDown (can move non-card parts down after non-card parts)', () => {
            let script = `tell button "Button 1" of current stack to moveDown`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentCard.sendMessage(msg, currentCard);
            orderCheck(currentStack, 0, "Current Card");
            orderCheck(currentStack, 2, "Card 2");
            orderCheck(currentStack, 1, "Card 3");
            orderCheck(currentStack, 4, "Button 1");
            orderCheck(currentStack, 3, "Button 2");
        });
    });
    describe('Subpart re-order with stacks', () => {
        let world;
        let currentStack;
        let currentStackView;
        let currentCardView;
        let  orderCheck = function(parentPart, newIndex, name){
            let parentView = System.findViewById(parentPart.id);
            let part = parentPart.subparts[newIndex];
            let nameProp = part.partProperties.getPropertyNamed(part, "name");
            assert.equal(nameProp, name);
            let view = System.findViewById(part.id);
            assert.equal(view, parentView.childNodes[newIndex]);
            assert.equal(part.id, view.getAttribute("part-id"));
        };
        before('', () => {
            world = System.partsById['world'];
            currentCard = System.getCurrentCardModel();
            currentCardView = System.findViewById(currentCard.id);
            currentStack = System.getCurrentStackModel();
            currentStack.partProperties.setPropertyNamed(currentStack, "name", "Current Stack");
            currentStackView = System.findViewById(currentStack.id);
            let initSemantics = function(){
                semantics = getSemanticsFor(currentCard);
            };
            expect(initSemantics).to.not.throw();
            let script = `add stack "Stack 2" to this world`;
            let match = testLanguageGrammar.match(script, 'Command');
            let msg = semantics(match).interpret();
            currentCard.sendMessage(msg, currentCard);
            script = `add stack "Stack 3" to this world`;
            match = testLanguageGrammar.match(script, 'Command');
            msg = semantics(match).interpret();
            currentCard.sendMessage(msg, currentCard);
        });
        it('Check that subpart and DOM child order is in sync', () => {
            orderCheck(world, 0, "Current Stack");
            orderCheck(world, 1, "Stack 2");
            orderCheck(world, 2, "Stack 3");
        });
        it('MoveToLast', () => {
            let script = `tell current stack to moveToLast`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentStack.sendMessage(msg, currentStack);
            // check order
            orderCheck(world, 2, "Current Stack");
            orderCheck(world, 0, "Stack 2");
            orderCheck(world, 1, "Stack 3");
        });
        it('MoveToFirst', () => {
            let script = `tell current stack to moveToFirst`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentStack.sendMessage(msg, currentStack);
            // check order
            orderCheck(world, 0, "Current Stack");
            orderCheck(world, 1, "Stack 2");
            orderCheck(world, 2, "Stack 3");
        });
        it('MoveDown', () => {
            let script = `tell current stack to moveDown`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentStack.sendMessage(msg, currentStack);
            // check order
            orderCheck(world, 1, "Current Stack");
            orderCheck(world, 0, "Stack 2");
            orderCheck(world, 2, "Stack 3");
        });
        it('MoveUp', () => {
            let script = `tell current stack to moveUp`;
            let match = testLanguageGrammar.match(script, 'Command');
            assert.isTrue(match.succeeded());
            let msg = semantics(match).interpret();
            assert.exists(msg);
            currentStack.sendMessage(msg, currentStack);
            // check order
            orderCheck(world, 0, "Current Stack");
            orderCheck(world, 1, "Stack 2");
            orderCheck(world, 2, "Stack 3");
        });
    });
});


<|start_filename|>js/objects/views/editors/EditorLocationInfo.js<|end_filename|>
import interpreterSemantics from '../../../ohm/interpreter-semantics.js';
import {onLocationLinkClick, getLocationStringFor} from './utils/subparts.js';

// PREAMBLE
const arrowLeftIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-narrow-left" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <line x1="5" y1="12" x2="19" y2="12" />
  <line x1="5" y1="12" x2="9" y2="16" />
  <line x1="5" y1="12" x2="9" y2="8" />
</svg>
`;

const clipboardIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" />
  <rect x="9" y="3" width="6" height="4" rx="2" />
</svg>
`;

const templateString = `
<style>
    :host(.hidden){
        display: none;
    }
    .button-link {
        display: inline-flex;
        align-items: center;
        outline: none;
        border: none;
        border-bottom: 1px solid rgba(150, 150, 150, 0.3);
        transition: border 0.2s ease-out;
        vertical-alignment: center;
        background-color: transparent;
        padding: 0px;
        font-size: 1em;
    }

    .button-link:hover {
        cursor: pointer;
        border-bottom: 1px solid rgba(150, 150, 150, 0.7);
        transition: border 0.2s ease-out;
    }

    .button-link > svg {
        margin-left: 8px;
        opacity: 0.7;
        transform: translateX(0px);
        transition: transform 0.2s ease-out, opacity 0.2s ease-out;
    }

    .button-link:hover > svg {
        opacity: 1.0;
        transform: translateX(-5px);
        transition: transform 0.2s ease-out, opacity 0.2s ease-out;
    }
</style>
<p class="part-info">
    My <button id="owner-link" class="button-link" title=""><span></span>${arrowLeftIcon}</button> is named <span class="part-name"></span> and is located at <button id="location-link" class="button-link" title="Copy location"><span></span>${clipboardIcon}</button> <button id="id-link" class="button-link" title="Copy id"><span>Copy id</span>${clipboardIcon}</button>
</p>
`;

class EditorLocationInfo extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Accepted values for the kind attribute
        this.allowedKinds = ['stack', 'card', 'owner'];

        // define and bind methods
        this.getLocationStringFor = getLocationStringFor.bind(this);
        this.onLocationLinkClick = onLocationLinkClick.bind(this);

        // Bound methods
        this.handleStackKind = this.handleStackKind.bind(this);
        this.handleCardKind = this.handleCardKind.bind(this);
        this.updateInfo = this.updateInfo.bind(this);
        this.getAncestorOfTypeFor = this.getAncestorOfTypeFor.bind(this);
        this.getLocationViews = this.getLocationViews.bind(this);
        this.onLinkClick = this.onLinkClick.bind(this);
        this.onMouseEnter = this.onMouseEnter.bind(this);
        this.onMouseLeave = this.onMouseLeave.bind(this);
    }

    connectedCallback(){
        // Events
        let ownerLinkButton = this._shadowRoot.getElementById('owner-link');
        let locationLinkButton = this._shadowRoot.getElementById('location-link');
        let idLinkButton = this._shadowRoot.getElementById('id-link');
        ownerLinkButton.addEventListener('click', this.onLinkClick);
        locationLinkButton.addEventListener('click', this.onLocationClick);
        idLinkButton.addEventListener('click', this.onLocationClick);
        locationLinkButton.addEventListener('mouseenter', this.onMouseEnter);
        idLinkButton.addEventListener('mouseenter', this.onMouseEnter);
        locationLinkButton.addEventListener('mouseleave', this.onMouseLeave);
        idLinkButton.addEventListener('mouseleave', this.onMouseLeave);
    }

    disconnectedCallback(){
        let ownerLinkButton = this._shadowRoot.getElementById('owner-link');
        let locationLinkButton = this._shadowRoot.getElementById('location-link');
        let idLinkButton = this._shadowRoot.getElementById('id-link');
        locationLinkButton.removeEventListener('click', this.onLocationClick);
        idLinkButton.removeEventListener('click', this.onLocationClick);
        ownerLinkButton.removeEventListener('mouseenter', this.onMouseEnter);
        locationLinkButton.removeEventListener('mouseenter', this.onMouseEnter);
        idLinkButton.removeEventListener('mouseenter', this.onMouseEnter);
        locationLinkButton.removeEventListener('mouseleave', this.onMouseLeave);
        idLinkButton.removeEventListener('mouseleave', this.onMouseLeave);
    }

    render(aModel){
        this.model = aModel;
        let kind = this.getAttribute('kind');
        if(!kind || !this.allowedKinds.includes(kind)){
            this.classList.add('hidden');
            return;
        }
        if(this.model.type == 'world'){
            this.classList.add('hidden');
            return;
        }

        this.classList.remove('hidden');

        // Update element references
        this.ownerLinkButton = this._shadowRoot.getElementById('owner-link');
        this.ownerLinkTypeSpan = this.ownerLinkButton.querySelector('span');
        this.locationLinkButton = this._shadowRoot.getElementById('location-link');
        this.locationLinkSpan = this.locationLinkButton.querySelector('span');
        this.idLinkButton = this._shadowRoot.getElementById('id-link');
        this.idLinkSpan = this.idLinkButton.querySelector('span');
        this.nameSpan = this._shadowRoot.querySelector('p .part-name');

        if(kind == 'stack'){
            this.handleStackKind();
        } else if(kind =='card'){
            this.handleCardKind();
        } else {
            this.updateInfo();
        }
    }

    updateInfo(){
        let kind = this.getAttribute('kind');
        let ancestor = this.model._owner;
        if(kind == 'stack' || kind == 'card'){
            ancestor = this.getAncestorOfTypeFor(this.model, kind);
        }

        // If we cannot find an ancestor of the given
        // kind, then we hide this field
        if(!ancestor){
            this.classList.add('hidden');
            return;
        }

        // Update name span
        let ancestorName = ancestor.partProperties.getPropertyNamed(
            ancestor,
            'name'
        );
        if(!ancestorName){
            ancestorName = '(unnamed)';
        } else {
            ancestorName = `"${ancestorName}"`;
        }
        this.nameSpan.textContent = ancestorName;

        // Update kind span
        let kindLabel = kind[0].toUpperCase() + kind.slice(1);
        this.ownerLinkTypeSpan.textContent = kindLabel;

        // Update location link span
        this.locationLinkSpan.textContent = this.getLocationStringFor(ancestor);

        // Update button titles
        let editTitle = `Edit owning ${kindLabel}`;
        if(kind == 'owner'){
            editTitle = 'Edit Owner';
        }
        this.ownerLinkButton.setAttribute(
            'title',
            editTitle
        );

        // Add the ref-id attribute
        this.setAttribute('ref-id', ancestor.id);
    }

    handleStackKind(){
        if(this.model.type == 'stack' || this.model.type == 'world'){
            this.classList.add('hidden');
            return;
        }
        this.updateInfo();
    }

    handleCardKind(){
        if(this.model.type == 'card' || this.model.type == 'stack'){
            this.classList.add('hidden');
            return;
        }
        this.updateInfo();
    }

    getAncestorOfTypeFor(aPart, aType){
        let result;
        let currentOwner = aPart._owner;
        while(currentOwner){
            if(currentOwner.type == aType){
                result = currentOwner;
                break;
            }
            currentOwner = currentOwner._owner;
        }
        return result;
    }

    onLinkClick(event){
        let id = this.getAttribute('ref-id');
        if(id && this.model){
            // Re-render the editor on the Part
            // referenced by the found id
            let target = window.System.partsById[id];
            target.partProperties.setPropertyNamed(target, "editor-open", true);
        }
    }

    onMouseEnter(event){
        this.getLocationViews(event).forEach((view) => {
            view.highlight("rgb(54, 172, 100)"); // green
        });
    }

    onMouseLeave(event){
        this.getLocationViews(event).forEach((view) => {
            view.unhighlight();
        });
    }

    getLocationViews(event){
        let targetId;
        let span = event.currentTarget.querySelector('span');
        if(span.parentElement.id == 'id-link'){
            targetId = this.getAttribute('ref-id');
        } else {
            let semantics = window.System.grammar.createSemantics();
            semantics.addOperation(
                'interpret',
                interpreterSemantics(window.System.partsById['world'], window.System)
            );
            let m = window.System.grammar.match(span.textContent, "ObjectSpecifier");
            try{
                targetId = semantics(m).interpret();
            } catch(e){
                console.log(`cannot locate ${span.textContent}`);
            }
        }
        return window.System.findViewsById(targetId);
    }
};

export {
    EditorLocationInfo,
    EditorLocationInfo as default
};


<|start_filename|>js/objects/tests/test-command-handler-recompilation-with-preload.js<|end_filename|>
/**
 * Command Handler and Script Recompilation
 * ---------------------------------------------
 * Tests that ensure correct behavior during
 * compilation and especially recompilation of
 * an object's script / command handlers
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

let currentCard;
let button;
describe('Can compile some basic custom command handlers', () => {
    describe('Setup', () => {
        it('Can add a new button to the current card', () => {
            let makeButtonFunction = function(){
                currentCard = System.getCurrentCardModel();
                let msg = {
                    type: 'command',
                    commandName: 'newModel',
                    args: ['button', currentCard.id]
                };
                currentCard.sendMessage(msg, currentCard);
            };
            expect(makeButtonFunction).to.not.throw();
        });
        it('Can find the button object, which has NO command handlers yet', () => {
            button = System.getCurrentCardModel().subparts.find(subpart => {
                return subpart.type == 'button';
            });
            assert.exists(button);
            assert.isEmpty(button._commandHandlers);
        });
    });
    describe('Can compile a basic script with two handlers', () => {
        it('Can compile the script without error', () => {
            let firstScript = [
                'on doSomethingFirst',
                'answer "first handler!"',
                'end doSomethingFirst',
                '\n',
                'on doSomethingElse someArg',
                'answer someArg',
                'end doSomethingElse'
            ].join('\n');
            let compileFunction = function(){
                let msg = {
                    type: 'compile',
                    targetId: button.id,
                    codeString: firstScript
                };
                currentCard.sendMessage(msg, currentCard);
            };
            expect(compileFunction).to.not.throw();
        });
        it('Button has two handlers with correct names', () => {
            expect(button._commandHandlers).to.have.all.keys('doSomethingFirst', 'doSomethingElse');
        });
        it('Can recompile the script with just one handler now', () => {
            let secondScript = [
                'on doSomethingFirst',
                'answer "first handler!"',
                'end doSomethingFirst'
            ].join('\n');
            let compileFunction = function(){
                let msg = {
                    type: 'compile',
                    targetId: button.id,
                    codeString: secondScript
                };
                currentCard.sendMessage(msg, currentCard);
            };
            expect(compileFunction).to.not.throw();
        });
        it('Should only have one handler present', () => {
            expect(button._commandHandlers).to.have.all.keys('doSomethingFirst');
            expect(button._commandHandlers).to.not.have.key('doSomethingElse');
        });
    });
});


<|start_filename|>js/objects/parts/WorldStack.js<|end_filename|>
/**
 * WorldStack
 * ---------------------------------------------------
 * I am a Stack part that represents the root of a
 * hierarchy of parts. I am the end of the ownership
 * chain for any given configuration of Parts.
 * I am also the final resolver of all unhandled
 * messages sent along the delegation chain for parts.
 * All parts can eventually resolve to me via the delegation
 * chain or ownership hierarchy.
 * There should only be one instance of me in any given
 * SimpleTalk environment.
 */
import Part from './Part.js';
import {
    BasicProperty,
} from '../properties/PartProperties.js';


class WorldStack extends Part {
    constructor(){
        super(null);

        this.acceptedSubpartTypes = ["stack", "image"];

        this.isWorld = true;

        // This property specifies the stack
        // id of the current stack
        this.partProperties.newBasicProp(
            'current',
            null
        );

        // Set the id property to always
        // be 'world'
        this.id = 'world';

        // Bind general methods
        this.sendOpenStackTo = this.sendOpenStackTo.bind(this);
        this.sendCloseStackTo = this.sendCloseStackTo.bind(this);

        // Bind navigation methods
        this.goToNextStack = this.goToNextStack.bind(this);
        this.goToPrevStack = this.goToPrevStack.bind(this);
        this.goToNthStack = this.goToNthStack.bind(this);
        this.goToStackById = this.goToStackById.bind(this);

        // properties
        this.partProperties.addProperty(
            new BasicProperty(
                "navigator-open",
                false
            )
        );

        // private command handlers
        this.setPrivateCommandHandler("openNavigator", () => {
            this.partProperties.setPropertyNamed(this, "navigator-open", true);
        });
        this.setPrivateCommandHandler("closeNavigator", () => {
            this.partProperties.setPropertyNamed(this, "navigator-open", false);
        });


        // remove command handlers which are not needed for world
        this.removePrivateCommandHandler("moveUp");
        this.removePrivateCommandHandler("moveDown");
        this.removePrivateCommandHandler("moveToFirst");
        this.removePrivateCommandHandler("moveToLast");

        // remove the halo property and command handlers as these do not makes sense here
        let haloOpenProp = this.partProperties.findPropertyNamed("halo-open");
        if(haloOpenProp){
            this.partProperties.removeProperty(haloOpenProp);
        }
        this.removePrivateCommandHandler("openHalo");
        this.removePrivateCommandHandler("closeHalo");
    }

    goToNextStack(){
        let stacks = this.subparts.filter(subpart => {
            return subpart.type == 'stack';
        });
        if(stacks.length < 2){
            return;
        }
        let currentStack = this.currentStack;
        let currentStackId = this.currentStackId;
        let currentIdx = stacks.indexOf(currentStack);
        let nextIdx = currentIdx + 1;
        if(nextIdx >= stacks.length){
            nextIdx = (nextIdx % stacks.length);
        }
        let nextStack = stacks[nextIdx];
        this.partProperties.setPropertyNamed(
            this,
            'current',
            nextStack.id
        );
        if(currentStackId != nextStack.id){
            this.sendCloseStackTo(currentStack);
            this.sendOpenStackTo(nextStack);
        }
    }

    goToStackById(anId){
        let stacks = this.subparts.filter(subpart => {
            return subpart.type == 'stack';
        });
        let nextStack = stacks.find(stack => {
            return stack.id == anId;
        });
        if(!nextStack){
            throw new Error(`The stack id: ${anId} cant be found on this stack`);
        }
        let currentStack = this.currentStack;
        let currentStackId = this.currentStackId;
        this.partProperties.setPropertyNamed(
            this,
            'current',
            nextStack.id
        );
        if(currentStackId != nextStack.id){
            this.sendCloseStackTo(currentStack);
            this.sendOpenStackTo(nextStack);
        }
    }

    goToPrevStack(){
        let stacks = this.subparts.filter(subpart => {
            return subpart.type == 'stack';
        });
        if(stacks.length < 2){
            return;
        }
        let currentStack = this.currentStack;
        let currentStackId = this.currentStackId;
        let currentIdx = stacks.indexOf(currentStack);
        let nextIdx = currentIdx - 1;
        if(nextIdx < 0){
            nextIdx = stacks.length + nextIdx;
        }
        let nextStack = stacks[nextIdx];
        this.partProperties.setPropertyNamed(
            this,
            'current',
            nextStack.id
        );
        if(currentStackId != nextStack.id){
            this.sendCloseStackTo(currentStack);
            this.sendOpenStackTo(nextStack);
        }
    }

    goToNthStack(anIndex){
        // NOTE: We are using 1-indexed values
        // per the SimpleTalk system
        let trueIndex = anIndex - 1;
        let stacks = this.subparts.filter(subpart => {
            return subpart.type == 'stack';
        });
        if(trueIndex < 0 || trueIndex > stacks.length -1){
            throw new Error(`Cannot navigate to stack number ${anIndex} -- out of bounds`);
        }
        let currentStack = this.currentStack;
        let currentStackId = this.currentStackId;
        let nextStack = stacks[trueIndex];
        this.partProperties.setPropertyNamed(
            this,
            'current',
            nextStack.id
        );
        if(currentStackId != nextStack.id){
            this.sendCloseStackTo(currentStack);
            this.sendOpenStackTo(nextStack);
        }
    }

    sendCloseStackTo(aStack){
        this.sendMessage({
            type: 'command',
            commandName: 'closeStack',
            args: [],
            shouldIgnore: true
        }, aStack);
        let currentCard = aStack.currentCard;
        if(currentCard){
            aStack.sendMessage({
                type: 'command',
                commandName: 'closeCard',
                args: [],
                shouldIgnore: true
            }, aStack.currentCard);
        }
    }

    sendOpenStackTo(aStack){
        this.sendMessage({
            type: 'command',
            commandName: 'openStack',
            args: [],
            shouldIgnore: true
        }, aStack);
        let currentCard = aStack.currentCard;
        if(currentCard){
            aStack.sendMessage({
                type: 'command', 
                commandName: 'openCard',
                args: [],
                shouldIgnore: true
            }, aStack.currentCard);
        }
    }

    get type(){
        return 'world';
    }

    get loadedStacks(){
        return this.subparts.filter(subpart => {
            return subpart.type == 'stack';
        });
    }

    // Override normal Part serialization.
    // Here we need to also include an array of ids of
    // loaded stacks and the id of the current stack
    serialize(){
        let result = {
            type: this.type,
            id: this.id,
            properties: {},
            subparts: this.subparts.map(subpart => {
                return subpart.id;
            }),
            ownerId: null,
            loadedStacks: (this.loadedStacks.map(stack => {
                return stack.id;
            })),
        };

        // Serialize current part properties
        // values
        this.partProperties._properties.filter(prop => {
            // we make sure to only serialize those properties which have non-default values
            // with the exception of the "custom-properties" prop which we always serialize
            return (prop.name == "custom-properties" || prop.getValue(this) !== prop.default);
        }).forEach(prop => {
            let name = prop.name;
            // due to race-conditions incurred if the editor is
            // open before world we exclude `editor-open` from serialization
            if(name == "editor-open"){
                return;
            }
            let value = prop.getValue(this);
            result.properties[name] = value;
        });
        return result;
    }

    // Override for delegation.
    // We send any messages that should be delegated
    // to the global System object, which has any
    // 'handlers of last resort'
    delegateMessage(aMessage){
        return this.sendMessage(aMessage, window.System);
    }

    get currentStackId(){
        return this.partProperties.getPropertyNamed(
            this,
            'current'
        );
    }

    get currentStack(){
        return window.System.partsById[this.currentStackId];
    }

    // override the base class methods
    addPart(aPart){
        if(!this.acceptsSubpart(aPart.type)){
            throw new Error(`${this.type} does not accept subparts of type ${aPart.type}`);
        }

        let found = this.subparts.indexOf(aPart);
        if(found < 0){
            // if the part is a stack then append after the last stack
            if(aPart.type == "stack"){
                let allStacks = this.subparts.filter((part) => {
                    return part.type == "stack";
                });
                this.subparts.splice(allStacks.length, 0, aPart);
            } else {
                this.subparts.push(aPart);
            }
            aPart._owner = this;
        }
    }

    moveSubpartDown(part){
        let currentIndex = this.subparts.indexOf(part);
        let lastValidPartIndex = this.subparts.length - 1;
        if(part.type == "stack"){
            let allStacks = this.subparts.filter((part) => {
                return part.type == "stack";
            });
            lastValidPartIndex = allStacks.length - 1;
        }
        if(currentIndex < lastValidPartIndex){
            this.subpartOrderChanged(part.id, currentIndex, currentIndex + 1);
        }
    }

    moveSubpartToLast(part){
        let currentIndex = this.subparts.indexOf(part);
        let lastValidPartIndex = this.subparts.length - 1;
        if(part.type == "stack"){
            let allStacks = this.subparts.filter((part) => {
                return part.type == "stack";
            });
            lastValidPartIndex = allStacks.length - 1;
        }
        if(currentIndex < lastValidPartIndex){
            this.subpartOrderChanged(part.id, currentIndex, lastValidPartIndex);
        }
    }

    moveSubpartUp(part){
        let currentIndex = this.subparts.indexOf(part);
        let firstValidPartIndex = 0;
        if(part.type != "stack"){
            let allStacks = this.subparts.filter((part) => {
                return part.type == "stack";
            });
            firstValidPartIndex = allStacks.length;
        }
        if(currentIndex > firstValidPartIndex){
            this.subpartOrderChanged(part.id, currentIndex, currentIndex - 1);
        }
    }

    moveSubpartToFirst(part){
        let currentIndex = this.subparts.indexOf(part);
        let firstValidPartIndex = 0;
        if(part.type != "stack"){
            let allStacks = this.subparts.filter((part) => {
                return part.type == "stack";
            });
            firstValidPartIndex = allStacks.length;
        }
        if(currentIndex > firstValidPartIndex){
            this.subpartOrderChanged(part.id, currentIndex, firstValidPartIndex);
        }
    }

};

export {
    WorldStack,
    WorldStack as default
};


<|start_filename|>js/objects/views/editors/EditorCustomList.js<|end_filename|>


// PREAMBLE

const caretDownIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M18 15l-6 -6l-6 6h12" transform="rotate(180 12 12)" />
</svg>
`;

const caretRightIcon = `
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  <path d="M18 15l-6 -6l-6 6h12" transform="rotate(90 12 12)" />
</svg>
`;

const templateString = `
<style>
    :host {
        display: flex;
        flex-direction: column;
        width: 100%;
        height: 100%;
        font-family: 'Helvetica', sans-serif;
        font-size: 0.8rem;
    }

    #props-list {
        flex: 1;
        overflow-y: auto;
        list-style: none;
        margin: 0;
        padding: 0;
        overflow-y: auto;
    }
    #filter-area {
        display: flex;
        width: 100%;
        align-items: center;
    }
    #filter-area > input {
        min-width: 0;
        width: auto;
        flex: 1;
        outline: none;
        font-size: 1.0rem;
        padding-left: 6px;
        padding-right: 6px;
        padding-top: 3px;
        padding-bottom: 3px;
        border: 1px solid rgba(100, 100, 100, 0.8);
        border-radius: 2px;
    }
    
    #new-prop-area {
        display: flex;
        flex-direction: column;
        width: 100%;
    }

    #new-prop-area.open > #new-prop-form {
        display:flex;
    }

    #new-prop-form {
        display: none;
        flex-direction: column;
        width: 100%;
    }

    .row {
        display: flex;
        flex-direction: row;
        width: 100%;
        align-items: center;
        margin-bottom: 1em;
    }

    h3 {
        padding: 0px;
        margin-bottom: 1em;
    }

    #new-prop-form > .row > * {
        margin-right: 16px;
        padding-left: 8px;
        padding-right: 8px;
    }
    #new-prop-form input {
        outline: none;
        border: 1px solid transparent;
        border-bottom: 1px solid rgba(100, 100, 100, 0.7);
        font-family: monospace;
        padding: 6px;
    }

    #new-prop-name:invalid {
        border-bottom: 1px solid red;
    }
    
    select {
        font-size: 1em;
    }
    #add-prop-dropdown-control {
        align-items: center;
        user-select: none;
    }

    #add-prop-dropdown-control:hover,
    #add-prop-dropdown-control label {
        cursor: pointer;
    }

    #caret-button {
        position: relative;
        width: 1.5em;
        height: 1.5em;
        transform: rotate(0deg);
        transition: transform 0.1s linear;
    }

    #caret-button > svg {
        width: 100%;
        height: 100%;
    }
    #new-prop-area.open #caret-button {
        transform: rotate(90deg);
        transition: transform 0.1s linear;
    }

</style>
<div id="new-prop-area">
    <div class="row" id="add-prop-dropdown-control">
        <h3><label for="caret-button">Add New Property</label></h3>
        <div id="caret-button">${caretRightIcon}</div>
    </div>
    <div id="new-prop-form">
        <div class="row">
            <label for="new-prop-name">Property Name </label>
            <input type="text" id="new-prop-name" placeholder="property-name" pattern="[a-z\\-]{3,64}"/>
        </div>
        <div class="row">
            <label for="default-val-select">Default value type</label>
            <select id="default-val-select">
                <option value="" selected>None</option>
                <option value="string">Text</option>
                <option value="number">Number</option>
                <option value="boolean">True or False</option>
            </select>
        </div>
        <div class="row">
            <label for="default-value">Default value </label>
            <input type="text" id="default-value" placeholder="Default value" disabled/>
        </div>
        <div class="row" id="submit-control">
            <button id="submit-prop">Create</button>
        </div>
    </div>
</div>
<div id="filter-area">
    <input type="text" id="filter-input" name="filter-input" placeholder="Filter..."/>
    <button id="clear">Clear</button>
</div>
<ul id="props-list">
    <slot></slot>
</ul>
`;

class EditorCustomList extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Bound methods
        this.render = this.render.bind(this);
        this.resetForm = this.resetForm.bind(this);
        this.onDefaultNewTypeChange = this.onDefaultNewTypeChange.bind(this);
        this.onCaretClick = this.onCaretClick.bind(this);
        this.onCreateSubmit = this.onCreateSubmit.bind(this);
        this.onFilterInput = this.onFilterInput.bind(this);
        this.onFilterClearClick = this.onFilterClearClick.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.newPropTypeSelect = this._shadowRoot.getElementById('default-val-select');
            this.newPropNameInput = this._shadowRoot.getElementById('new-prop-name');
            this.newPropDefaultValue = this._shadowRoot.getElementById('default-value');
            this.addPropControl = this._shadowRoot.getElementById('add-prop-dropdown-control');
            this.newPropForm = this._shadowRoot.getElementById('new-prop-form');
            this.createButton = this._shadowRoot.getElementById('submit-prop');
            this.clearButton = this._shadowRoot.getElementById('clear');
            this.filterInput = this._shadowRoot.getElementById('filter-input');
            
            // Add listeners
            this.addPropControl.addEventListener('click', this.onCaretClick);
            this.createButton.addEventListener('click', this.onCreateSubmit);
            this.filterInput.addEventListener('input', this.onFilterInput);
            this.clearButton.addEventListener('click', this.onFilterClearClick);
        }
    }

    render(aModel){
        this.model = aModel;

        // Clear any main dom children
        this.innerHTML = "";

        // Create a sorted copy of the property
        // objects
        let customProps = this.model.partProperties.getPropertyNamed(
            this.model,
            'custom-properties'
        );

        // Create a sorted list of the custom properties
        // available
        this.propList = Object.values(customProps)
            .sort((first, second) => {
                return first.name.localeCompare(second.name);
            });

        // Render the property items and insert them
        this.propList.forEach(propObject => {
            let el = document.createElement('editor-prop-item');
            el.setProperty(propObject, this.model);
            el.setAttribute('name', propObject.name);
            this.appendChild(el);
        });

        // Set up event listeners
        this.newPropTypeSelect.removeEventListener('change', this.onDefaultNewTypeChange);
        this.newPropTypeSelect.addEventListener('change', this.onDefaultNewTypeChange);
    }

    onDefaultNewTypeChange(event){
        let option = event.target.selectedOptions[0];
        switch(option.value){
        case 'string':
            this.newPropDefaultValue.setAttribute('type', 'text');
            this.newPropDefaultValue.setAttribute('value', "");
            break;
        case 'number':
            this.newPropDefaultValue.setAttribute('type', 'number');
            this.newPropDefaultValue.setAttribute('value', 0);
            break;
        case 'boolean':
            this.newPropDefaultValue.setAttribute('type', 'checkbox');
            break;
        default:
            this.newPropDefaultValue.setAttribute('type', 'text');
        }

        if(option.value == ""){
            this.newPropDefaultValue.setAttribute('disabled', true);
        } else {
            this.newPropDefaultValue.removeAttribute('disabled');
        }
    }

    onCaretClick(event){
        let newPropArea = this._shadowRoot.getElementById('new-prop-area');
        newPropArea.classList.toggle('open');
    }

    onCreateSubmit(event){
        if(this.model){
            let propName = this.newPropNameInput.value;
            let defaultValue = this.newPropDefaultValue.value;
            if(this.newPropDefaultValue.type == 'checkbox'){
                defaultValue = this.newPropDefaultValue.checked;
            } else if(this.newPropDefaultValue.type == 'number'){
                defaultValue = parseFloat(this.newPropDefaultValue.value);
            }

            // Send the property create message
            this.model.sendMessage({
                type: 'command',
                commandName: 'newProperty',
                args: [
                    propName,
                    this.model.id
                ]
            }, this.model);

            // Set the created prop to the default
            // value
            this.model.partProperties.setPropertyNamed(
                this.model,
                propName,
                defaultValue
            );

            // Re-render this pane
            this.resetForm();
            this.render(this.model);
        }
    }

    filterBy(text){
        // Find all of the prop item elements whose
        // property name does *not* include the substring,
        // and set those to not display
        let allElements = Array.from(this.querySelectorAll('editor-prop-item'));
        allElements.forEach(propEl => {
            let name = propEl.getAttribute('name');
            if(name.toLowerCase().includes(text)){
                propEl.classList.remove('item-hidden');
            } else {
                propEl.classList.add('item-hidden');
            }
        });
    }

    onFilterInput(event){
        this.filterBy(event.target.value.toLowerCase());
    }

    onFilterClearClick(event){
        this.filterInput.value = "";
        this.filterBy("");
    }

    resetForm(){
        this.newPropNameInput.value = null;
        this.newPropDefaultValue.value = null;
        if(this.newPropDefaultValue.type == 'checked'){
            this.newPropDefaultValue.checked = false;
        }
        this.newPropDefaultValue.type = 'text';
        this.newPropDefaultValue.setAttribute('disabled', true);
        this.newPropTypeSelect.selectedIndex = 0;
    }
};

export {
    EditorCustomList,
    EditorCustomList as default
};


<|start_filename|>js/objects/utils/merriamInterface.js<|end_filename|>
const merriamSimScore = async (sender, docId) => {
    const url = "https://patents.merriamtech.com/_api/merriam/"
    const payload = {
        "fields": [
            "title",
            "date_publ"
        ],
        "weights": {
            "merriam":0.7,
            "date":0.3,
            "hierarchy":0.1
        },
        "doc_ids": [
            docId
        ],
        "limit": 5}
    const params = {
        "method": "POST",
        "headers": {
            "content-type": "application/json"
        },
        "body": JSON.stringify(payload)
    }
    fetch(url, params).then(data => {
        return data.json();
    }).then(json => {
        const msg = {
            type: 'command',
            commandName: 'merriamresult',
            args: [
                JSON.stringify(json)
            ]
        };
        sender.sendMessage(msg, sender);
    });
}

export {
    merriamSimScore,
    merriamSimScore as default
};


<|start_filename|>js/objects/tests/test-window-part.js<|end_filename|>
/**
 * Window Part Tests
 * -----------------------------------
 * Basic tests of the Window Part.
 * Here we are just ensuring basic validity
 * checks and property setting
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

import Window from '../parts/Window.js';

// Import valid subpart kinds
import Area from '../parts/Area.js';
import Stack from '../parts/Stack';

// Import one invalid subpart kind
import Button from '../parts/Button.js';

let windowModel;
describe('Window Part Tests', () => {
    it('#addPart will add Area as a subpart successfully', () => {
        let win = new Window();
        let area = new Area();
        let addPartFunc = function(){
            win.addPart(area);
        };

        expect(addPartFunc).to.not.throw(Error);
    });

    it('#addPart will NOT add a stack Part successfully', () => {
        let win = new Window();
        let Stack;
        let addPartFunc = function(){
            stack = new Stack(win);
            win.addPart(stack);
        };

        expect(addPartFunc).to.throw(Error);
    });

    it('#setTarget sets target object', () => {
        let targetPart = new Stack();
        let win = new Window();

        win.setTarget(targetPart);

        assert.equal(win.target, targetPart);
    });

    it('#setTarget sets targetId part property to target parts id', () => {
        let targetPart = new Stack();
        let win = new Window();
        win.setTarget(targetPart);

        let foundId = win.partProperties.getPropertyNamed(
            win,
            'targetId'
        );

        assert.equal(foundId, targetPart.id);
    });
});


<|start_filename|>js/objects/views/editors/utils/subparts.js<|end_filename|>
 const getLocationStringFor = (aPart) => {
        let result = "";
        let currentPart = aPart;
        let currentOwner = aPart._owner;
        while(currentOwner){
            let indexInParent = currentOwner.subparts.filter((subpart) => {
                return subpart.type == currentPart.type;
            }).indexOf(currentPart) + 1;
            result += `${currentPart.type} ${indexInParent} of `;
            currentPart = currentPart._owner;
            currentOwner = currentOwner._owner;
        }
        result += 'this world';
        return result;
}


const onLocationLinkClick = (event) => {
        let text = event.currentTarget.querySelector('span').textContent;
        let input = document.createElement('input');
        input.style.position = 'absolute';
        input.style.opacity = 0;
        document.body.append(input);
        let currentFocus = document.activeElement;
        input.focus();
        input.value = text;
        console.log(input.value);
        input.select();
        document.execCommand('copy');
        input.remove();
        currentFocus.focus();
}

export {
    getLocationStringFor,
    onLocationLinkClick
};


<|start_filename|>js/objects/utils/serialization.js<|end_filename|>
/**
 * Serialization and Deserialization Utility Classes
 */
import idMaker from './id.js';

const version = "0.0.2";

class STDeserializer {
    constructor(aSystem){
        this.system = aSystem;

        // These caches are used during the process
        // as optimizations
        this._modelCache = {};
        this._subpartMapCache = {};
        this._idCache = {};
        this._instanceCache = [];
        this._propsCache = {};
        this._viewsCache = {};
        this._scriptCache = {};
        this._rootsCache = [];

        // The targetId is the id of
        // the Part that we wish to append any
        // deserialized subpart tree into.
        // By default we assume the whole system,
        // ie full deserialization.
        this.targetId = 'system';
        // the root id is the id of the root part instance
        // being attached
        this.rootId = null;

        // Bound methods
        this.deserialize = this.deserialize.bind(this);
        this.deserializeData = this.deserializeData.bind(this);
        this.deserializePart = this.deserializePart.bind(this);
        this.attachSubparts = this.attachSubparts.bind(this);
        this.setProperties = this.setProperties.bind(this);
        this.createView = this.createView.bind(this);
        this.attachView = this.attachView.bind(this);
        this.setViewModel = this.setViewModel.bind(this);
        this.compilePartScript = this.compilePartScript.bind(this);
        this.refreshWorld = this.refreshWorld.bind(this);
        this.appendWorld = this.appendWorld.bind(this);
        this.addPartsToSystem = this.addPartsToSystem.bind(this);
        this.compileScripts = this.compileScripts.bind(this);
        this.getFlattenedPartTree = this.getFlattenedPartTree.bind(this);
        this.getModelClass = this.getModelClass.bind(this);
        this.handleId = this.handleId.bind(this);
        this.throwError = this.throwError.bind(this);
        this.flushCaches = this.flushCaches.bind(this);
        this.dispatchViewAdded = this.dispatchViewAdded.bind(this);
    }

    deserialize(aJSONString){
        this.data = JSON.parse(aJSONString);
        let target = this.system.partsById[this.targetId];
        return this.deserializeData()
            .then(() => {
                // Add all deserialized Parts to the System dict,
                // including the new World.
                this.addPartsToSystem(this._instanceCache);
            })
            .then(() => {
                // Compile the scripts on *all* deserialized
                // parts
                this.compileScripts(this._instanceCache);
            })
            .then(() => {
                // Insert the root Part into whatever
                // target it should go into.
                let rootPart = this.rootParts[0];
                let rootView = this.rootViews[0];
                if(this.targetId == 'system'){
                    this.refreshWorld();
                } else {
                    target.addPart(rootPart);
                }

                // Finally, append the PartView root node
                // where it should go in the view tree.
                if(this.targetId == 'system'){
                    this.appendWorld();
                } else {
                    let targetView = document.querySelector(`[part-id="${this.targetId}"]`);
                    targetView.appendChild(rootView);
                    this.dispatchViewAdded(rootView);
                    rootPart.sendMessage({
                        type: 'command',
                        commandName: 'newModel',
                        args: [],
                        shouldNotDelegate: true,
                        shouldIgnore: true
                    }, rootPart);
                }
                return this;
            });
    }

    deserializeData(){
        return new Promise((resolve, reject) => {
            this.flushCaches();
            // First, we ensure that the target we
            // should be deserializing into actually exists
            let target = this.system.partsById[this.targetId];
            if(!target && this.targetId != 'system'){
                this.throwError(`Target id ${this.targetId} does not exist in System`);
            }

            // Second, we create instances of all models in the serialization
            // but we do not yet attach their subparts.
            Object.values(this.data.parts).forEach(partData => {
                this.deserializePart(Object.assign({}, partData));
            });

            // Translate targets
            for (let modelId in this._propsCache) {
                let props = this._propsCache[modelId];
                if (props.target !== null) {
                    for (let oldId in this._idCache) {
                        let newId = this._idCache[oldId];
                        if (props.target === 'part id ' + oldId) {
                            props.target = 'part id ' + newId;
                            break;
                        }
                    }
                }
            }
            // Translate scripts
            for (let modelId in this._scriptCache) {
                let script = this._scriptCache[modelId];
                if (script && script.match('part id') !== null) {
                    for (let oldId in this._idCache) {
                        let newId = this._idCache[oldId];
                        let oldRe = 'part id ' + oldId;
                        let newRe = 'part id ' + newId;
                        if (this._scriptCache[modelId].match(oldRe) !== null) {
                            console.log(modelId);
                            let re = new RegExp(oldRe, "g");
                            this._scriptCache[modelId] = script.replace(re, newRe);
                        }
                    }
                }
            }

            // Third, we go through each created Part instance
            // and add any subparts to it. Note that this is not
            // recursive
            this._instanceCache.forEach(partInstance => {
                this.attachSubparts(partInstance);
            });

            // Forth and fifth. Create and attach views
            // Note this is recursive to preserve the subpart + view children order
            let root = this._instanceCache.filter((part) => {
                return part.partProperties.getPropertyNamed(part, "id") == this.rootId;
            })[0];
            this.createAndAttachViews(root);

            // Sixth, we set all properties on each created
            // Part model from the deserialized data.
            // We do this using a visitor method on the instances
            // themselves.
            // This gives the in-memory views the ability to
            // react to any initial changes to their models.
            this._instanceCache.forEach(partInstance => {
                this.setProperties(partInstance);
                // We need to translate new ids to old ones
                if (partInstance.name == "WorldStack") {
                    let world = partInstance;
                    world.partProperties.setPropertyNamed(
                        world,
                        "current",
                        this._idCache[world.currentStackId]
                    );
                }
                if (partInstance.name == "Stack") {
                    let stack = partInstance;
                    stack.partProperties.setPropertyNamed(
                        stack,
                        "current",
                        this._idCache[stack.currentCardId]
                    );
                }
                this.setViewModel(partInstance);
            });

            // We determine which of the instances is a "root",
            // meaning that it has, at this point, no owner in
            // the deserialized data. There can be multiple roots
            // (and therefore multiple trees) in a single deserialization
            this._rootsCache = this._instanceCache.filter(instance => {
                return instance._owner == null || instance._owner == undefined;
            });

            // Insertion should be handled by composed
            // promises elsewhere (see imports and deserialize()
            // for examples)

            return resolve(this);
        });
    }

    createAndAttachViews(partInstance){
        this.createView(partInstance);
        this.attachView(partInstance);
        if(partInstance.subparts.length){
            partInstance.subparts.forEach((subpartInstance) => {
                this.createAndAttachViews(subpartInstance);
            });
        }
    }

    importFromSerialization(aJSONString, filterFunction){
        this.data = JSON.parse(aJSONString);
        let target = this.system.partsById[this.targetId];
        let targetView = document.querySelector(`[part-id="${this.targetId}"]`);
        return this.deserializeData()
            .then(() => {
                // The caller will provide a filter function over
                // all deserialized part instances, returning only
                // those that should be inserted into the target.
                // For example, all Stacks in the WorldStack.
                return this._instanceCache.filter(filterFunction);
            })
            .then((rootParts) => {
                rootParts.forEach(rootPart => {
                    let allTreeParts = this.getFlattenedPartTree(rootPart);
                    this.addPartsToSystem(allTreeParts);
                });
                return rootParts;

            })
            .then((rootParts) => {
                rootParts.forEach(rootPart => {
                    let allTreeParts = this.getFlattenedPartTree(rootPart);
                    this.compileScripts(allTreeParts);
                });
                return rootParts;
            })
            .then((rootParts) => {
                rootParts.forEach(rootPart => {
                    let view = this._viewsCache[rootPart.id];
                    target.addPart(rootPart);
                    targetView.appendChild(view);
                    this.dispatchViewAdded(view);
                });
            });
    }

    deserializePart(partData){
        let partClass = this.getModelClass(partData.type);
        let instance = new partClass();

        // We create a new ID for this part, since we cannot
        // guarantee ID clashes with the existing System.
        // Exception is if the useOriginalids flag is set,
        // such as at load time
        let {newId, oldId} = this.handleId(instance, partData);
        instance.id = newId;
        // cache the new root ID if this is a root instance
        if(this.data.rootId == oldId){
            this.rootId = newId;
        }

        // Add to our caches and also to the System
        this._idCache[oldId] = newId;
        this._scriptCache[newId] = partData.properties.script;
        this._propsCache[newId] = partData.properties;
        this._modelCache[newId] = instance;
        this._subpartMapCache[newId] = partData.subparts;
        this._instanceCache.push(instance);
    }

    handleId(aPart, partData){
        let newId, oldId;
        oldId = partData.id;
        newId = aPart.id;
        if(aPart.type !== 'world'){
            newId = idMaker.new();
        }
        return {
            newId,
            oldId
        };
    }

    addPartsToSystem(aListOfParts){
        aListOfParts.forEach(part => {
            this.system.partsById[part.id] = part;
        });
    }

    compileScripts(aListOfParts){
        aListOfParts.forEach(part => {
            this.compilePartScript(part);
        });
    }

    attachSubparts(aPart){
        // At this point, the _subpartMapCache should
        // have an entry mapping from this aPart's (new)
        // id to an array of ids of also-initialized
        // subpart models
        let subpartIds = this._subpartMapCache[aPart.id];
        subpartIds.forEach(subpartId => {
            let newId = this._idCache[subpartId];
            let subpartModel = this._modelCache[newId];
            if(!subpartModel){
                debugger;
            }
            aPart.addPart(subpartModel);
        });
    }

    setProperties(aPart){
        let props = this._propsCache[aPart.id];
        delete props['id'];
        aPart.setPropsFromDeserializer(props, this);
    }

    createView(aPart){
        let newView = document.createElement(
            this.system.tagNameForViewNamed(aPart.type)
        );
        // we need to set the part-id attribute since these
        // are used for queries needed for things like
        // current stack and card
        newView.setAttribute("part-id", aPart.id);
        this._viewsCache[aPart.id] = newView;
    }

    setViewModel(aPart){
        let view = this._viewsCache[aPart.id];
        view.setModel(aPart);
    }
    
    attachView(aPart){
        let owner = aPart._owner;
        if(owner){
            let ownerView = this._viewsCache[owner.id];
            let partView = this._viewsCache[aPart.id];
            owner.sendMessage({
                type: "viewChanged",
                changeName: "subpart-new",
                args: [partView]
            }, ownerView);
        }
    }

    compilePartScript(aPart){
        let scriptString = this._scriptCache[aPart.id];
        if(scriptString && scriptString != ""){
            this.system.compile({
                type: 'compile',
                targetId: aPart.id,
                codeString: scriptString,
                serialize: false
            });
        }
    }

    refreshWorld(){
        // We assume a single root part was deserialized and
        // attach it as the World accordingly
        let newWorld = this.rootParts[0];
        if(newWorld.type !== 'world'){
            this.throwError(`Found ${this.rootParts.length} roots, but no world!`);
        }
        this.system.partsById['world'] = this.rootParts[0];
    }

    appendWorld(){
        // We assume a single root view that is an st-world.
        let found = document.querySelector('st-world');
        if(found){
            document.body.replaceChild(this.rootViews[0], found);
        } else {
            document.body.prepend(this.rootViews[0]);
        }
        this.dispatchViewAdded(document.querySelector('st-world'));
    }

    getFlattenedPartTree(aPart, list=[]){
        list.push(aPart);
        aPart.subparts.forEach(subpart => {
            this.getFlattenedPartTree(subpart, list);
        });
        return list;
    }

    throwError(message){
        throw new Error(`Deserialization Error: ${message}`);
    }

    getModelClass(aPartTypeStr){
        let cls = this.system.availableParts[aPartTypeStr];
        if(!cls){
            this.throwError(`Part type "${aPartTypeStr}" does not exist in system`);
        }
        return cls;
    }

    flushCaches(){
        this._modelCache = {};
        this._subpartMapCache = {};
        this._idCache = {};
        this._instanceCache = [];
        this._propsCache = {};
        this._viewsCache = {};
        this._scriptCache = {};
        this._rootsCache = [];
    }

    dispatchViewAdded(aView){
        let event = new CustomEvent('st-view-added', {
            detail: {
                partType: aView.model.type,
                partId: aView.model.id,
                //ownerId: aView.model._owner.id || null
            } 
        });
        aView.parentElement.dispatchEvent(event);
    }

    get rootParts(){
        return this._rootsCache;
    }

    get rootViews(){
        return this.rootParts.map(part => {
            return this._viewsCache[part.id];
        });
    }
}


class STSerializer {
    constructor(aSystem){
        this.system = aSystem;
        this._objectCache = {};

        // Bound methods
        this.serializePart = this.serializePart.bind(this);
        this.flushCaches = this.flushCaches.bind(this);
    }

    serialize(aRootPart, pretty=true){
        this.flushCaches();
        let result = {
            version: version,
            rootId: aRootPart.id,
            type: aRootPart.type,
            id: aRootPart.id
        };

        // Recursively serialize Parts and
        // store in flat list
        this.serializePart(aRootPart);

        // We set the result objects parts
        // dict to be the same as the cache
        result.parts = this._objectCache;

        // Finally, we convert to a string and
        // return
        if(pretty){
            return JSON.stringify(result, null, 4);
        } else {
            return JSON.stringify(result);
        }
    }

    serializePart(aPart){
        // We use the serialize method available on
        // base Parts, passing in this serializer instance
        // as the sole arg
        this._objectCache[aPart.id] = aPart.serialize(this);
        aPart.subparts.forEach(subpart => {
            this.serializePart(subpart);
        });
    }

    flushCaches(){
        this._objectCache = {};
    }
}

export {
    STSerializer,
    STDeserializer
};


<|start_filename|>js/objects/views/editors/EditorSubpartItem.js<|end_filename|>
// PREAMBLE
import partIcons from '../../utils/icons.js';

const templateString = `
<style>

</style>
`;


<|start_filename|>js/objects/views/BackgroundView.js<|end_filename|>
import PartView from './PartView.js';

const templateString = `
                <style>
                </style>
                <slot></slot>
`;

class BackgroundView extends PartView {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );
    }
};

export {
    BackgroundView,
    BackgroundView as default
};


<|start_filename|>js/objects/views/contextmenu/ContextMenu.js<|end_filename|>
// PREAMBLE
import ContextMenuItem from './ContextMenuItem.js';

window.customElements.define('st-context-menu-item', ContextMenuItem);

const templateString = `
<style>
    :host {
        display: flex;
        flex-direction: column;
        position: absolute;
        border: 1px solid black;
        background-color: white;
        box-shadow: 1px 2px 10px rgba(50, 50, 50, 0.7);
        z-index: 10000;
        padding-bottom: 8px;
        min-width: 200px;
        font-family: 'Helvetica', sans-serif;
    }

    :host-context(li) {
        display: none;
        position: absolute;
        left: 100%;
        top: 0px;
    }

    :host-context(li):hover {
        display: flex;
    }

    header {
        position: relative;
        display: flex;
        border-bottom: 1px solid rgba(150, 150, 150, 0.5);
        padding-right: 16px;
        padding-left: 16px;
        padding-top: 8px;
        padding-bottom: 8px;
    }

    header > h4 {
        padding: 0;
        margin:0;
    }

    ul {
        list-style: none;
        margin: 0;
        padding: 0;
        font-size: 0.8rem;
    }

</style>
<header>
    <h4></h4>
</header>
<ul id="list-items">
    <slot></slot>
</ul>
`;

class ContextMenu extends HTMLElement {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Bound methods
        this.addHaloToggleItem = this.addHaloToggleItem.bind(this);
        this.addNavigatorToggleItem = this.addNavigatorToggleItem.bind(this);
        this.addCopyAndPasteItems = this.addCopyAndPasteItems.bind(this);
        this.addOpenEditorItem = this.addOpenEditorItem.bind(this);
        this.addScriptEditItem = this.addScriptEditItem.bind(this);
        this.addMovementItems = this.addMovementItems.bind(this);
        this.addPartSubmenu = this.addPartSubmenu.bind(this);
        this.addSaveSnapshotItem = this.addSaveSnapshotItem.bind(this);
        this.addListItem = this.addListItem.bind(this);
        this.addSpacer = this.addSpacer.bind(this);
        this.hideHeader = this.hideHeader.bind(this);
        this.adjustToClientView = this.adjustToClientView.bind(this);
    }

    render(aModel){
        this.innerHTML = "";
        this.model = aModel;
        let headerEl = this._shadowRoot.querySelector('header > h4');
        let headerText = `${this.model.type[0].toUpperCase()}${this.model.type.slice(1)}`;
        headerText = `a ${headerText}`;
        headerEl.textContent = headerText;

        // Render the default menu items
        this.addHaloToggleItem();
        this.addCopyAndPasteItems();
        this.addOpenEditorItem();
        this.addPartSubmenu();
        this.addScriptEditItem();
        this.addMovementItems();
        this.addNavigatorToggleItem();
        this.addSaveSnapshotItem();

        // Add View-specific items
        let view = document.querySelector(`[part-id="${this.model.id}"]`);
        view.addContextMenuItems(this);
    }

    addListItem(label, callback, submenu=null){
        let itemEl = document.createElement('st-context-menu-item');
        itemEl.textContent = label;
        itemEl.classList.add('context-menu-item');
        itemEl.addEventListener('click', callback);
        itemEl.addEventListener('click', () => {
            this.remove();
        });
        if(submenu){
            submenu.classList.add('context-submenu', 'submenu-hidden');
            submenu.setAttribute('slot', 'submenu');
            itemEl.append(submenu);
            itemEl.showCaret();
        }
        this.append(itemEl);
    }

    addSaveSnapshotItem(){
        this.addListItem(
            'Save Snapshot...',
            (event) => {
                window.System.world.sendMessage({
                    type: 'command',
                    commandName: 'saveHTML',
                    args: []
                }, window.System.world);
            }
        );
    }

    addHaloToggleItem(){
        let target = window.System.findViewById(this.model.id);
        // don't add halo option to cards, since you can't see those
        if(target.name != "CardView"){
            if(target.classList.contains('editing')){
                this.addListItem(
                    'Close Halo',
                    (event) => {
                        target.model.partProperties.setPropertyNamed(this.model, "halo-open", false);
                    }
                );
            } else {
                this.addListItem(
                    'Open Halo',
                    (event) => {
                        target.model.partProperties.setPropertyNamed(this.model, "halo-open", true);
                    }
                );
            }
        }
    }

    addNavigatorToggleItem(){
        let world = window.System.partsById['world'];
        let navOpen = world.partProperties.getPropertyNamed(world, "navigator-open");
        if(navOpen){
            this.addListItem(
                'Close Navigator',
                (event) => {
                    world.partProperties.setPropertyNamed(world, "navigator-open", false);
                }
            );
        } else {
            this.addListItem(
                'Open Navigator',
                (event) => {
                    world.partProperties.setPropertyNamed(world, "navigator-open", true);
                }
            );
        }
    }

    addCopyAndPasteItems(){
        // Add copy item
        this.addListItem(
            'Copy',
            (event) => {
                window.System.clipboard.copyPart(this.model);
            }
        );

        // Add paste but only if:
        // 1. There is clipboard contents;
        // 2. The part type in the clipboard is
        //    one that is accepted by this model's part
        if(window.System.clipboard.contents.length){
            let partType = window.System.clipboard.contents[0].partType;
            if(this.model.acceptsSubpart(partType)){
                let label = `Paste (a ${partType[0].toUpperCase()}${partType.slice(1)})`;
                this.addListItem(label, (event) => {
                    window.System.clipboard.pasteContentsInto(this.model);
                });
            }
        }
    }

    addOpenEditorItem(){
        this.addListItem(
            'Open Editor',
            (event) => {
                this.model.sendMessage({
                    type: 'command',
                    commandName: 'openEditor',
                    args: [this.model.id]
                }, this.model);
            }
        );
    }

    addScriptEditItem(){
        this.addListItem(
            'Edit Script',
            (event) => {
                this.model.sendMessage({
                    type: 'command',
                    commandName: 'openScriptEditor',
                    args: [this.model.id]
                }, this.model);
            }
        );

        this.addListItem(
            'Edit World Script',
            (event) => {
                this.model.sendMessage({
                    type: 'command',
                    commandName: 'openScriptEditor',
                    args: ['world']
                }, this.model);
            }
        );

        let windowAncestor = this.model.findAncestorOfType('window');
        if(this.model.type != 'window' && windowAncestor !== null){
            this.addListItem(
                'Edit Owning Window Script',
                (event) => {
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'openScriptEditor',
                        args: [windowAncestor.id]
                    }, this.model);
                }
            );
        }
        
        let cardAncestor = this.model.findAncestorOfType('card');
        if(this.model.type != 'card' && cardAncestor){
            this.addListItem(
                'Edit Owning Card Script',
                (event) => {
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'openScriptEditor',
                        args: [cardAncestor.id]
                    }, this.model);
                }
            );
        }

        let stackAncestor = this.model.findAncestorOfType('stack');
        if(this.model.type != 'stack' && stackAncestor){
            this.addListItem(
                'Edit Owning Stack Script',
                (event) => {
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'openScriptEditor',
                        args: [stackAncestor.id]
                    }, this.model);
                }
            );
        }
    }

    addPartSubmenu(){
        // First, we need to get a list of names
        // of subparts that this model accepts
        let subpartNames;
        if(this.model.acceptedSubpartTypes[0] == "*"){
            // This model accepts all subpart types.
            // We need to get the names for these subparts,
            // which are registered at the System level.
            subpartNames = Object.keys(window.System.availableViews);
        } else {
            subpartNames = this.model.acceptedSubpartTypes;
        }

        // If there are no subpart names (meaning
        // the given part, like a button, doesn't
        // accept any subparts), then we do nothing.
        if(subpartNames.length == 0){
            return;
        }

        // Now we construct the submenu for adding parts
        // of the given type
        let submenu = document.createElement('st-context-menu');
        submenu.hideHeader();
        subpartNames.forEach(subpartName => {
            submenu.addListItem(
                subpartName[0].toUpperCase() + subpartName.slice(1),
                () => {
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'newModel',
                        args: [
                            subpartName,
                            this.model.id
                        ]
                    }, this.model);
                }
            );
        });

        // Now add the list item that will "reveal"
        // the submenu
        this.addListItem(
            'Add a new part',
            null,
            submenu
        );
        
    }

    addMovementItems(){
        let index = this.model._owner.subparts.indexOf(this.model);
        let ownerLength = this.model._owner.subparts.length;
        if(ownerLength && index < ownerLength - 1){
            // Create the moveDown option
            this.addListItem(
                'Move Down',
                (event) => {
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'moveDown',
                        args: []
                    }, this.model);
                }
            );
            this.addListItem(
                'Move to Last',
                (event) => {
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'moveToLast',
                        args: []
                    }, this.model);
                }
            );
        }
        if(index > 0){
            // Create the moveUp option
            this.addListItem(
                'Move Up',
                (event) => {
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'moveUp',
                        args: []
                    }, this.model);
                }
            );
            this.addListItem(
                'Move to First',
                (event) => {
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'moveToFirst',
                        args: []
                    }, this.model);
                }
            );
        }
    }

    addSpacer(){
        let item = document.createElement('li');
        item.classList.add('context-menu-spacer');
        this.append(item);
    }

    hideHeader(){
        let headerEl = this._shadowRoot.querySelector('header');
        headerEl.style.display = "none";
    }

    adjustToClientView(){
        let rect = this.getBoundingClientRect();
        let padding = 10;
        let viewportWidth = document.documentElement.clientWidth;
        let viewportHeight = document.documentElement.clientHeight;
        let bottomDiff = (rect.bottom + padding) - viewportHeight;
        let rightDiff = (rect.right + padding) - viewportWidth;
        if(bottomDiff > 0){
            this.style.top = `${(rect.top - bottomDiff)}px`;

            // Reposition any hidden submenus, so they open
            // above
            Array.from(this.children).filter(childEl => {
                return childEl.children.length > 0;
            }).forEach(itemWithSubmenu => {
                let container = itemWithSubmenu._shadowRoot.querySelector('.submenu-area');
                container.style.top = `${(-1 * itemWithSubmenu.getBoundingClientRect().height)}px`;
            });
        }
        if(rightDiff > 0){
            this.style.left = `${rect.left - rightDiff}px`;

            // Reposition any hidden submenus, so they open
            // to the left (instead of right)
            Array.from(this.children).filter(childEl => {
                return childEl.children.length > 0;
            }).forEach(itemWithSubmenu => {
                let container = itemWithSubmenu._shadowRoot.querySelector('.submenu-area');
                container.style.left = `${(-1 * itemWithSubmenu.getBoundingClientRect().width)}px`;
            });
        }
    }
};

export {
    ContextMenu,
    ContextMenu as default
};


<|start_filename|>js/objects/tests/test-view-lifecycle.js<|end_filename|>
/**
 * PartView Lifecycle Tests
 * -------------------------------------
 * Here we test the desired ability of PartView
 * descendants to make use of the various lifecycle
 * methods we've built into the base PartView
 *
 */
import 'jsdom-global/register';
import chai from 'chai';
const assert = chai.assert;

import Part from '../parts/Part.js';
import PartView from '../views/PartView.js';

class TestView extends PartView {
    constructor(){
        super();
        this.afterModelSetCalled = false;
        this.afterConnectedCalled = false;
        this.afterDisconnectedCalled = false;
    }

    afterConnected(){
        this.afterConnectedCalled = true;
    }

    afterDisconnected(){
        this.afterDisconnectedCalled = true;
    }

    afterModelSet(){
        this.afterModelSetCalled = true;
    }
};

class TestPart extends Part {
    constructor(){
        super();
        this.partProperties.newBasicProp(
            'test-property',
            false
        );
    }
};

window.customElements.define('test-view', TestView);

describe('Basic View Lifecycle Methods', () => {
    describe('#afterModelSet', () => {
        after(() => {
            document.body.innerHTML = "";
        });
        it('Not called when first attached', () => {
            let testView = document.createElement('test-view');
            document.body.append(testView);
            assert.isFalse(testView.afterModelSetCalled);
        });
        it('Called after setting the model', () => {
            let dummyModel = new TestPart();
            let testView = document.body.querySelector('test-view');
            testView.setModel(dummyModel);
            assert.isTrue(testView.afterModelSetCalled);
        });
    });

    describe('#afterConnected', () => {
        let testView;
        after(() => {
            document.body.innerHTML = "";
        });
        it('Not called upon element creation', () => {
            testView = document.createElement('test-view');
            assert.isFalse(testView.afterConnectedCalled);
        });
        it('Called after being connected to a parent element', () => {
            document.body.append(testView);
            assert.isTrue(testView.afterConnectedCalled);
        });
        it.skip('Has correctly called PartView connectedCallback inits first', () => {
            // TODO: When we have initializations in #connectedCallback
            // of PartView, make sure we check for them here
            assert(true);
        });
    });

    describe('#afterDisconnected', () => {
        let testView;
        after(() => {
            document.body.innerHTML = "";
        });
        it('Not called when element created', () => {
            testView = document.createElement('test-view');
            assert.isFalse(testView.afterDisconnectedCalled);
        });
        it('Not called when element is attached to parent', () => {
            document.body.append(testView);
            assert.isFalse(testView.afterDisconnectedCalled);
        });
        it('Called after being removed from parent', () => {
            testView.remove();
            assert.isTrue(testView.afterDisconnectedCalled);
        });
    });
});


<|start_filename|>js/objects/parts/Background.js<|end_filename|>
/**
 * Background
 * ------------------------------------
 * I am a Card-like part that represents a
 * background layer for a given card.
 * My direct owner is always a Stack.
 * Stacks can contain several Background parts.
 * Cards in a Stack pick which Background in the Stack
 * they wish to use. By default, Stacks are created with
 * a "blank" first Background, and any new Cards in
 * the Stack will be set to use this first Background.
 * I have all the capabilities of a Card.
 * In terms of message delegation, all Card Parts
 * delegate to the (current) Background they have.
 * I then delegate to the Stack that is my owner.
 */
import Card from './Card.js';

class Background extends Card {
    constructor(owner, name){
        super(owner, name);

        this.isBackground = true;

        this.acceptedSubpartTypes = ["card"];
    }

    get type(){
        return 'background';
    }
};

export {
    Background,
    Background as default
};


<|start_filename|>js/objects/parts/Image.js<|end_filename|>
import {Part} from './Part.js';

import {
    addBasicStyleProps,
    addPositioningStyleProps
} from '../utils/styleProperties.js';

class Image extends Part {
    constructor(owner, src) {
        super(owner);

        // Properties
        this.partProperties.newBasicProp(
            "src",
            null
        );

        this.partProperties.newBasicProp(
            "mimeType",
            "unknown"
        );

        this.partProperties.newBasicProp(
            "imageData",
            null
        );

        this.partProperties.newBasicProp(
            'draggable',
            false
        );

        // Style properties
        addBasicStyleProps(this);
        addPositioningStyleProps(this);
        this.setupStyleProperties();
        // part specific default style properties
        this.partProperties.setPropertyNamed(
            this,
            'background-transparency',
            0,
            true, // notify
            true  // set default
        );
        this.partProperties.setPropertyNamed(
            this,
            'background-color',
            "black",
            true, // notify
            true  // set default
        );

        // Private command handlers
        this.setPrivateCommandHandler("loadImageFrom", this.loadImageFromSource);
        this.setPrivateCommandHandler("loadImageFromFile", this.loadImageFromFile);

        // Bind component methods
        this.loadImageFromSource = this.loadImageFromSource.bind(this);
        this.loadImageFromFile = this.loadImageFromFile.bind(this);
    }

    loadImageFromSource(senders, url){
        this.partProperties.setPropertyNamed(
            this,
            'src',
            url
        );
    }

    loadImageFromFile(){
        let filePicker = document.createElement('input');
        filePicker.type = 'file';
        filePicker.setAttribute('accept', 'image/*');
        filePicker.style.display = 'none';
        filePicker.addEventListener('change', (event) => {
            // Handle the file here
            let reader = new FileReader();
            reader.onloadend = () => {
                this.partProperties.setPropertyNamed(
                    this,
                    'mimeType',
                    filePicker.files[0].type
                );
                this.partProperties.setPropertyNamed(
                    this,
                    'imageData',
                    reader.result
                );
            };
            let imageFile = filePicker.files[0];
            if(imageFile.type.includes('svg')){
                reader.readAsText(imageFile);
            } else {
                reader.readAsDataURL(imageFile);
            }
            filePicker.remove();
        });
        document.body.append(filePicker);
        filePicker.click();
    }

    /**
     * Serialize this Part's state as JSON.
     * By default, we do not serialize specific
     * PartCollection information (recursively),
     * and only include basics including the current
     * state of all properties.
     * ---
     * Note: Here we override the default implementation
     * in order to account for src/imageData property
     * serialization logic
     */
    serialize(){
        let ownerId = null;
        if(this._owner){
            ownerId = this._owner.id;
        }
        let result = {
            type: this.type,
            id: this.id,
            properties: {},
            subparts: this.subparts.map(subpart => {
                return subpart.id;
            }),
            ownerId: ownerId
        };

        // Grab the current image src url value
        // and the current imageData value
        let url = this.partProperties.getPropertyNamed(
            this,
            'src'
        );
        let urlIsValid = (url && url !== "");
        let currentData = this.partProperties.getPropertyNamed(
            this,
            'imageData'
        );
        this.partProperties._properties.filter(prop => {
            // we make sure to only serialize those properties which have non-default values
            // with the exception of the "custom-properties" prop which we always serialize
            return (prop.name == "custom-properties" || prop.getValue(this) !== prop.default);
        }).forEach(prop => {
            let name = prop.name;
            // due to race-conditions incurred if the editor is
            // open before world we exclude `editor-open` from serialization
            if(name == "editor-open"){
                return;
            }
            let value = prop.getValue(this);
            if(name == "imageData"){
                if(!urlIsValid){
                    result.properties.imageData = value;
                    result.properties.src = null;
                } else {
                    result.properties.imageData = null;
                }
            } else {
                result.properties[name] = value;
            }
        });
        return result;
    }

    get type(){
        return 'image';
    }

    get isSvg(){
        let mimeType = this.partProperties.getPropertyNamed(
            this,
            "mimeType"
        );
        if(!mimeType){
            return false;
        }

        return mimeType.startsWith('image/svg');
    }
};

export {
    Image,
    Image as default
};


<|start_filename|>js/objects/properties/PartProperties.js<|end_filename|>
import cssStyler from '../utils//styler.js';

/**
 * PartProperties
 * ------------------------------------
 * I am an object representing the base
 * Part Properties for all Parts.
 * I also include some convenience methods
 * on my prototype that should be used by
 * other Parts when they inherit from me.
 * For now, we use Object.create() for inheritance.
 */

class BasicProperty {
    constructor(name, defaultValue, readOnly=false, aliases=[]){
        this.name = name;
        this._value = defaultValue;
        this.default = defaultValue;
        this.readOnly = readOnly;
        this.aliases = aliases;

        // Bound methods
        this.getValue = this.getValue.bind(this);
        this.setValue = this.setValue.bind(this);
        this.hasAlias = this.hasAlias.bind(this);
        this.matches = this.matches.bind(this);
        this.matchesNameOrAlias = this.matchesNameOrAlias.bind(this);
    }

    // For basic properties, we return
    // the set/stored value
    getValue(owner){
        return this._value;
    }

    // For the basic properties, we set
    // based on the incoming desired value
    // alone (nothing is computed)
    setValue(owner, val, notify=true, setDefault=false){
        if(!this.readOnly){
            this._value = val;
            if(notify){
                owner.propertyChanged(
                    this.name,
                    val
                );
            }
            if(setDefault){
                this.default = val;
            }
        }
    }

    // Returns true if this property
    // goes by the given alias
    hasAlias(anAlias){
        return this.aliases.includes(anAlias);
    }

    // Returns true if the given name is
    // either an alias or the exact name
    // for this property
    matchesNameOrAlias(aNameOrAlias){
        if(this.hasAlias(aNameOrAlias)){
            return true;
        } else if(aNameOrAlias == this.name){
            return true;
        }
        return false;
    }

    // Returns true if the incoming Property
    // has the same name and/or one of the same
    // aliases as this Property
    matches(aProperty){
        if(aProperty.name == this.name){
            return true;
        } else {
            for(let i = 0; i < this.aliases.length; i++){
                let myAlias = this.aliases[i];
                if(aProperty.hasAlias(myAlias)){
                    return true;
                }
            }
        }
        return false;
    }
};


/** Custom Properties are similar to dynamic props, except that
  * under the hood they store an object of properties
  * storing props defined within the ST environment. The find()
  * add() delete() methods find, add, or remove properties from the
  * this.customProperties object.
 **/
class CustomProperty extends BasicProperty {
    constructor(name="custom-properties", defaultValue={}, readOnly=false, aliases=[]){
        super(name, defaultValue, readOnly=false, aliases);
    }

    find(name){
        let prop = this._value[name];
        if(prop){
            return prop;
        }
        return null;
    }

    add(aProperty){
        // NOTE: aliases are completed ignored for now
        if(!this.find(aProperty.name)){
            this._value[aProperty.name] = aProperty;
        }
    }

    delete(aProperty){
        delete this._value[aProperty.name];
    }



};

class DynamicProperty extends BasicProperty {
    constructor(name, setter, getter, readOnly=false, defaultValue=null, aliases=[]){
        super(name, defaultValue, readOnly, aliases);
        this.valueSetter = setter;
        this.valueGetter = getter;
    }

    // In this override, we use the getter
    // if available, to dynamically get the
    // incoming value.
    getValue(owner){
        return this.valueGetter(owner, this);
    }

    // In this override, we use the setter
    // if available, to dynamically set the
    // incoming value
    setValue(owner, val, notify=true, setDefault=false){
        if(!this.readOnly){
            this.valueSetter(owner, this, val, notify);
            if(notify){
                owner.propertyChanged(
                    this.name,
                    val
                );
            }
            if(setDefault){
                this.default = val;
            }
        }
    }
};


/** I am a special property which handles interfacing with the
  * the cssStyle basic property. Whenever I am updated I make
  * sure to update the cssStyle property via the styler utility
  * function. I can be used to create different and indepent
  * styling options.
  **/
class StyleProperty extends BasicProperty {
    constructor(name, defaultValue,  propName='cssStyle', styler=cssStyler, readOnly=false, aliases=[]){
        super(name, defaultValue, readOnly, aliases);
        this.propName = propName;
        this.styler = styler;
    }

    // In this override, we update the cssStyle property
    setValue(owner, val, notify=true, setDefault=false){
        if(!this.readOnly){
            let styleProperty = owner.partProperties.findPropertyNamed(this.propName);
            let style = styleProperty.getValue(owner);
            let newStyle = this.styler(style, this.name, val);
            styleProperty.setValue(owner, newStyle, notify, false); // do not set as default

            // set my value as well
            this._value = val;
            if(notify){
                owner.propertyChanged(
                    this.name,
                    val
                );
            }
            if(setDefault){
                this.default = val;
            }
        }
    }
};

class PartProperties {
    constructor(){
        this._properties = [];

        // Bound methods
        this.hasProperty = this.hasProperty.bind(this);
        this.addProperty = this.addProperty.bind(this);
        this.removeProperty = this.removeProperty.bind(this);
        this.findPropertyNamed = this.findPropertyNamed.bind(this);
        this.setPropertyNamed = this.setPropertyNamed.bind(this);
        this.getPropertyNamed = this.getPropertyNamed.bind(this);
        this.newBasicProp = this.newBasicProp.bind(this);
        this.newStyleProp = this.newStyleProp.bind(this);
        this.newDynamicProp = this.newDynamicProp.bind(this);
        this._indexOfProperty = this._indexOfProperty.bind(this);
    }

    get all(){
        return this._properties;
    }

    // This collection 'has' a property if it contains
    // a Property object with matching name or alias
    // of the incoming property.
    hasProperty(aProperty){
        for(let i = 0; i < this._properties.length; i++){
            let prop = this._properties[i];
            if(aProperty.matches(prop)){
                return true;
            }
        }
        return false;
    }

    // Find one of my Properties by
    // a name or alias. Returns null
    // if no match found. Perhaps we should
    // throw an error
    findPropertyNamed(aName){
        let found = null;
        let customPropertiesProp;
        for(let i = 0; i < this._properties.length; i++){
            let prop = this._properties[i];
            if(prop.matchesNameOrAlias(aName)){
                found = prop;
            }
            // grab the custom properties prop, as we might need it
            // for later
            if(prop.matchesNameOrAlias("custom-properties")){
                customPropertiesProp = prop;
            }
        }
        // see if the property is custom
        if(!found && customPropertiesProp){
            found = customPropertiesProp.find(aName);
        }
        return found;
    }

    // Attempts to get the *value* of the property
    // with the given name or alias.
    // If the property is not found, we throw an
    // error
    getPropertyNamed(owner, aName){
        let found = this.findPropertyNamed(aName);
        if(!found){
            throw new Error(`${owner} does not have property "${aName}"`);
        }
        return found.getValue(owner);
    }

    // Attempts to set the *value* of the property
    // with the given name or alias.
    // If the property is not found, we throw an
    // error
    setPropertyNamed(owner, aName, aValue, notify=true, setDefault=false){
        let found = this.findPropertyNamed(aName);
        if(!found){
            throw new Error(`${owner} does not have property "${aName}"`);
        }
        return found.setValue(owner, aValue, notify, setDefault);
    }

    // If you add a property with a name or alias
    // that is already present in the collection,
    // then we 'overwrite' it by removing the exising
    // property and replacing it with the incoming one.
    // Otherwise, we just add the property
    addProperty(aProperty){
        if(this.hasProperty(aProperty)){
            this.removeProperty(aProperty);
        }
        this._properties.push(aProperty);
    }

    // Removing a property here means removing
    // it from the stored array. If the property
    // is not in the array, we do NOT throw an error.
    // We just go on with our lives, because who cares?
    removeProperty(aProperty){
        let propIndex = this._indexOfProperty(aProperty);
        if(propIndex >= 0){
            this._properties.splice(propIndex, 1);
        }
    }

    // Convenience method for creating a new basic
    // property.
    newBasicProp(...args){
        let newProp = new BasicProperty(...args);
        this.addProperty(newProp);
    }

    // Convenience method for creating a new custom
    // property.
    newCustomProp(...args){
        let newProp = new CustomProperty(...args);
        this.addProperty(newProp);
    }

    // Convenience method for creating a new style
    // property.
    newStyleProp(...args){
        let newProp = new StyleProperty(...args);
        this.addProperty(newProp);
    }

    // Convenience method for creating a new
    // dynamic prop
    newDynamicProp(...args){
        let newProp = new DynamicProperty(...args);
        this.addProperty(newProp);
    }

    // Private method. Finds the first occurring
    // index of the given Property in the array
    // of properties in this collection. Returns
    // -1 if not found, per JS implementation.
    _indexOfProperty(aProperty){
        for(let i = 0; this._properties.length; i++){
            let prop = this._properties[i];
            if(aProperty.matches(prop)){
                return i;
            }
        }
        return -1;
    }
};

export {
    PartProperties,
    BasicProperty,
    CustomProperty,
    DynamicProperty,
    PartProperties as default
};


<|start_filename|>js/objects/utils/AltSyntaxHighlighter.js<|end_filename|>
/** Second pass at syntax highlighter semantics **/
const syntaxSpan = (ruleName) => {
    let span = document.createElement('span');
    span.classList.add('st-syntax');
    span.setAttribute('data-st-rule', ruleName);
    return span;
}

const createHighlighter = (fieldElement) => {
    return {
        MessageHandlerOpen: function(literalOn, messageName, optionalParamList){
            let span = syntaxSpan("MessageHandlerOpen");
            let onSpan = syntaxSpan("keyword");
            onSpan.append("on ");
            span.append(onSpan);

            // Append sub-rules
            span.append(messageName.highlightSyntax());
            span.append(...optionalParamList.highlightSyntax());

            return span;
        },

        MessageHandlerClose(literalEnd, messageName){
            let span = syntaxSpan("MessageHandlerClose");
            let endSpan = syntaxSpan("keyword");
            endSpan.append("end ");
            span.append(endSpan);

            // Add the parts
            span.append(messageName.highlightSyntax());

            return span;
        },

        ParameterList: function(paramString){
            let outer = syntaxSpan("ParameterList");
            let innerItems = paramString.asIteration().children.map(paramName => {
                let span = syntaxSpan("ParameterList-item");
                span.append(paramName.sourceString);
                return span.outerHTML;
            });
            outer.innerHTML = innerItems.join(", ");
            return outer;
            
        },

        messageName: function(string){
            let span = document.createElement('span');
            span.classList.add('st-syntax');
            span.setAttribute('data-st-rule', 'messageName');
            span.append(string.sourceString + " ");
            return span;
        },

        keyword: function(string){
            let span = document.createElement('span');
            span.classList.add('st-syntax');
            span.setAttribute('data-st-rule', 'keyword');
            span.append(string.sourceString);
            return span;
        }
    };
};

export {
    createHighlighter,
    createHighlighter as default
};


<|start_filename|>plugins/testAPI.js<|end_filename|>
const test = {
    name: "TestAPI",
    load: function(url){
        this.src = url;
        return true;
    },
    response: null,
    src: null,
    get: async function(prerequisite, key){
        if(key){
            if(!this.response){
                throw Error("No result has been fetched");
            }
            return this.response[key];
        }
        let src = this.src;
        if(prerequisite){
            src = src + "/" + prerequisite;
        }
        let response = await fetch(src);
        if (response.ok) { // if HTTP-status is 200-299
            // get the response body (the method explained below)
            let json = await response.json();
            this.response = json;
            return JSON.stringify(json);
        } else {
            console.error("HTTP-Error: " + response.status);
            return false;
        };
    },
};

export {
    test,
    test as default
};


<|start_filename|>js/objects/views/drawing/ColorWheelWidget.js<|end_filename|>
/**
 * ColorWheelWidget
 * I am a *generic use* webcomponent representing
 * a ColorWheel selection widget.
 * I operate as a floating modal window with a
 * circular color wheel as well as a list of
 * recently selected colors.
 * I am designed to be used by any parent element.
 * I will trigger an event called 'color-change' whenever
 * a new color has been selected from within me
 */

const colorWheelTemplate = `
<style>
  :host {
    display: initial !important;
    visibility: visible !important;
    position: relative;
    flex-direction: column;
    align-items: center;
    box-sizing: border-box;
    background-color: white;
    border: 1px solid black;
  }

  #palette-bar {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    height: 20px;
    background-color: rgba(200, 200, 200);
    width: 100%;
    box-sizing: border-box;
  }

  #palette-title {
    width: 100%;
    text-align: center;
  }

  #close-button {
    display: block;
    width: 12px;
    height: 12px;
    margin-left: 8px;
    background-color: white;
    border: 1px solid black;
    text-align: center;
    font-size: 12px;
  }

  #palette-content {
    flex: 1;
    display: block;
    position: relative;
  }
  #hover-color {
    display: block;
    width: 100%;
    height: 25px;
  }
  #options {
    display: flex;
    width: 100%;
    height: 25px;
    margin-top: 5px;
    justify-content: center;
  }

  #options > label{
    font-size: .8rem;
    display: flex;
    align-items: center;
  }

  #recent-colors {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-around;
    padding-left: 15px;
    padding-right: 15px;
    box-sizing: border-box;
    list-style: none;
  }
  .recent-color-item {
    display: block;
    width: 26px;
    height: 26px;
    box-sizing: border-box;
    border: 1px solid rgba(200, 200, 200, 0.8);
  }

  .recent-color-item.selected {
    border: 2px solid black;
  }

</style>
<div id="palette-wrapper">
  <div id="palette-bar"><div id="close-button">x</div><span id="palette-title"></span></div>
  <div id="palette-content">
    <div id="options">
      <input type="range" id="transparency" name="transparency" min="0" max="1" step="0.1" value="1">
      <!-- <label for="transparency">Transparency</label>-->
    </div>
    <ul id="recent-colors">
      <li class="recent-color-item selected"></li>
      <li class="recent-color-item"></li>
      <li class="recent-color-item"></li>
    </ul>
    <canvas id="color-wheel" width="150" height="150"></canvas>
    <div id="hover-color"></div>
  </div>
</div>
`;

class ColorWheelWidget extends HTMLElement {
    constructor(name){
        super();

        this.name = name;

        // Setup shadow dom and template
        this.template = document.createElement('template');
        this.template.innerHTML = colorWheelTemplate;
        this.shadow = this.attachShadow({mode: 'open'});
        this.shadow.append(
            this.template.content.cloneNode(true)
        );

        // Bind local methods
        this.onWheelMouseEnter = this.onWheelMouseEnter.bind(this);
        this.onWheelMouseLeave = this.onWheelMouseLeave.bind(this);
        this.onWheelMouseMove = this.onWheelMouseMove.bind(this);
        this.onWheelClick = this.onWheelClick.bind(this);
        this.onItemClick = this.onItemClick.bind(this);
        this.onBarMouseDown = this.onBarMouseDown.bind(this);
        this.onBarMouseUp = this.onBarMouseUp.bind(this);
        this.onBarMouseMove = this.onBarMouseMove.bind(this);
        this.onClose = this.onClose.bind(this);
        this.onTransparencyChange = this.onTransparencyChange.bind(this);
        this._drawWheel = this._drawWheel.bind(this);
    }

    connectedCallback(){
        if(this.isConnected){
            this.canvas = this.shadowRoot.querySelector('canvas');
            this.bar = this.shadowRoot.getElementById('palette-bar');
            // give the widget a title if provided
            if(this.name){
                this.shadowRoot.getElementById('palette-title').innerText = this.name;
            }

            // Set events
            this.canvas.addEventListener('click', this.onWheelClick);
            this.canvas.addEventListener('mouseenter', this.onWheelMouseEnter);
            this.bar.addEventListener('mousedown', this.onBarMouseDown);
            Array.from(this.shadowRoot.querySelectorAll('.recent-color-item')).forEach(el => {
                el.addEventListener('click', this.onItemClick);
            });
            this.transparencySlider = this.shadowRoot.getElementById('transparency');
            this.transparencySlider.addEventListener("input", this.onTransparencyChange);

            // Draw the color wheel to the canvas
            this._drawWheel();
        }
    }

    disconnectedCallback(){
        this.canvas.removeEventListener('click', this.onWheelClick);
        this.canvas.removeEventListener('mouseenter', this.onWheelMouseEnter);
        this.bar.removeEventListener('mousedown', this.onBarMouseDown);
        Array.from(this.shadowRoot.querySelector('.recent-color-item')).forEach(el => {
            el.removeEventListener('click', this.onItemClick);
        });
        this.transparencySlider.removeEventListener("change", this.onTransparencyChange);
    }


    onWheelMouseEnter(event){
        // Cache the image data for the whole canvas
        let ctx = this.canvas.getContext('2d');
        this._cachedImageData = ctx.getImageData(
            0,
            0,
            this.canvas.width,
            this.canvas.height
        );
        // Bind subsequent events
        this.canvas.addEventListener('mousemove', this.onWheelMouseMove);
        this.canvas.addEventListener('mouseleave', this.onWheelMouseLeave);
    }

    onWheelMouseLeave(event){
        this.canvas.removeEventListener('mousemove', this.onWheelMouseMove);
        this.canvas.removeEventListener('mouseleave', this.onWheelMouseLeave);
        this._cachedImageData = null;
    }

    onWheelMouseMove(event){
        let position = getPositionFromEvent(event);
        let rgb = getRGBFromImageData(
            position.x,
            position.y,
            this.canvas.width,
            this._cachedImageData.data
        );
        let hoverColorArea = this.shadowRoot.getElementById('hover-color');
        let newStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${rgb[3]})`;
        hoverColorArea.style.backgroundColor = newStyle;
    }

    onWheelClick(event){
        let position = getPositionFromEvent(event);
        let rgb = getRGBFromImageData(
            position.x,
            position.y,
            this.canvas.width,
            this._cachedImageData.data
        );
        let colorInfo = {
            r: rgb[0],
            g: rgb[1],
            b: rgb[2],
            alpha: rgb[3]
        };
        let newEvent = new CustomEvent('color-selected', {
            detail: colorInfo
        });
        this.selectedColor = colorInfo;
        this.dispatchEvent(newEvent);

        // Update the recent color swatches
        let currentSwatchSelection = this.shadowRoot.querySelector('.recent-color-item.selected');
        if(currentSwatchSelection){
            currentSwatchSelection.style.backgroundColor = `rgba(${colorInfo.r}, ${colorInfo.g}, ${colorInfo.b}, ${colorInfo.alpha})`;
            currentSwatchSelection.selectedColor = colorInfo;
        }
    }

    onTransparencyChange(event){
        let command = this.getAttribute("selector-command");
        // update the corresponding transparency - text or background
        // depending on what this color wheel is setup to update
        let propName = "background-transparency";
        if(command === "text-color"){
            propName = "text-transparency";
        }
        let eventDetail = {propName: propName, value: event.target.value};
        let newEvent = new CustomEvent('transparency-changed', {
            detail: eventDetail,
        });
        this.dispatchEvent(newEvent);
    }

    onItemClick(event){
        // If this element is not the currently
        // selected recent item, find the one that is
        // and toggle the selection class, then toggle
        // this item's selection class.
        if(!event.target.classList.contains('selected')){
            Array.from(this.shadowRoot.querySelectorAll('.recent-color-item.selected')).forEach(el => {
                el.classList.remove('selected');
            });
            event.target.classList.add('selected');
        }

        if(event.target.selectedColor){
            let newEvent = new CustomEvent('color-selected', {
                detail: event.target.selectedColor
            });
            this.dispatchEvent(newEvent);
        }
    }

    onBarMouseDown(event){
        document.addEventListener('mousemove', this.onBarMouseMove);
        document.addEventListener('mouseup', this.onBarMouseUp);
    }

    onBarMouseUp(event){
        document.removeEventListener('mousemove', this.onBarMouseMove);
        document.removeEventListener('mouseup', this.onBarMouseUp);
    }

    onBarMouseMove(event){
        let newTop = this.offsetTop + event.movementY;
        let newLeft = this.offsetLeft + event.movementX;
        this.style.top = `${newTop}px`;
        this.style.left = `${newLeft}px`;
    }

    onClose(event){
        this.remove();
    }

    _drawWheel(){
        let ctx = this.canvas.getContext('2d');
        drawCircle(ctx, this.canvas.width / 2);
    }
};

window.customElements.define('color-wheel', ColorWheelWidget);

/** Utility Functions **/

// This function is adapted from
// https://medium.com/@bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43
const drawCircle = (ctx, radius) => {
    let image = ctx.createImageData(2*radius, 2*radius);
    let data = image.data;

    for (let x = -radius; x < radius; x++) {
        for (let y = -radius; y < radius; y++) {

            let [r, phi] = xy2polar(x, y);

            if (r > radius) {
                // skip all (x,y) coordinates that are outside of the circle
                continue;
            }

            let deg = rad2deg(phi);

            // Figure out the starting index of this pixel in the image data array.
            let rowLength = 2*radius;
            let adjustedX = x + radius; // convert x from [-50, 50] to [0, 100] (the coordinates of the image data array)
            let adjustedY = y + radius; // convert y from [-50, 50] to [0, 100] (the coordinates of the image data array)
            let pixelWidth = 4; // each pixel requires 4 slots in the data array
            let index = (adjustedX + (adjustedY * rowLength)) * pixelWidth;

            let hue = deg;
            let saturation = r / radius;
            let value = 1.0;

            let [red, green, blue] = hsv2rgb(hue, saturation, value);
            let alpha = 255;

            data[index] = red;
            data[index+1] = green;
            data[index+2] = blue;
            data[index+3] = alpha;
        }
    }

    ctx.putImageData(image, 0, 0);
};

// This utility function is adapted from:
// https://medium.com/@bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43
const xy2polar = (x, y) => {
    let r = Math.sqrt(x*x + y*y);
    let phi = Math.atan2(y, x);
    return [r, phi];
};

// This utility function is adapted from:
// https://medium.com/@bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43s
const rad2deg = (rad) => {
    return ((rad + Math.PI) / (2 * Math.PI)) * 360;
};

// This utility function is adapted from:
// https://medium.com/@bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43
const hsv2rgb = (hue, saturation, value) => {
    let chroma = value * saturation;
    let hue1 = hue / 60;
    let x = chroma * (1- Math.abs((hue1 % 2) - 1));
    let r1, g1, b1;
    if (hue1 >= 0 && hue1 <= 1) {
        ([r1, g1, b1] = [chroma, x, 0]);
    } else if (hue1 >= 1 && hue1 <= 2) {
        ([r1, g1, b1] = [x, chroma, 0]);
    } else if (hue1 >= 2 && hue1 <= 3) {
        ([r1, g1, b1] = [0, chroma, x]);
    } else if (hue1 >= 3 && hue1 <= 4) {
        ([r1, g1, b1] = [0, x, chroma]);
    } else if (hue1 >= 4 && hue1 <= 5) {
        ([r1, g1, b1] = [x, 0, chroma]);
    } else if (hue1 >= 5 && hue1 <= 6) {
        ([r1, g1, b1] = [chroma, 0, x]);
    }

    let m = value - chroma;
    let [r,g,b] = [r1+m, g1+m, b1+m];

    // Change r,g,b values from [0,1] to [0,255]
    return [255*r,255*g,255*b];
};

const getPositionFromEvent = (event) => {
    let target = event.target;
    let offsetX = target.offsetLeft;
    let offsetY = target.offsetTop;
    let check = target.offsetParent;
    while(check){
        offsetX += check.offsetLeft;
        offsetY += check.offsetTop;
        check = check.offsetParent;
    }
    let result = {
        x: event.clientX - offsetX,
        y: event.clientY - offsetY
    };
    return result;
};

const getRGBFromImageData = (x, y, width, data) => {
    let index = (y * width + x) * 4;
    return [
        data[index], // r
        data[index + 1], // g
        data[index + 2], // b
        data[index + 3] // alpha
    ];
};

export {
    ColorWheelWidget,
    ColorWheelWidget as default
};


<|start_filename|>js/ohm/interpreter-semantics.js<|end_filename|>
import {ActivationContext} from '../objects/ExecutionStack.js';

// Helpers
function findNearestParentOfKind(aPart, aPartType){
    let owner = aPart._owner;
    while(owner){
        if(owner.type == aPartType){
            return owner;
        }
        owner = owner._owner;
    }
    throw new Error(`'this' is a ${aPart.type}, not a ${aPartType} or does not have a parent of a ${aPartType}!`);
}

// check for possibleAncestor.acceptsSubpart(aPart.type)
// and if not go to owner and check again
function findFirstPossibleAncestor(aPart, aPartType){
    if(_subpartCheck(aPart, aPartType)){
        return aPart;
    } else {
        let owner = aPart._owner;
        while(owner){
            if(_subpartCheck(owner, aPartType)){
                return owner;
            }
            owner = owner._owner;
        }
    }
    throw new Error(`a ${aPart.type}, does not accept nor has any ancestors which accept part type ${aPartType}`);
}

function _subpartCheck(aPart, aPartType){
    if(aPartType == 'part'){
        return aPart.acceptedSubpartTypes.length > 0;
    }
    return aPart.acceptsSubpart(aPartType);
}

class STVariableReferenceError extends Error {
    constructor(...args){
        super(...args);
    }
};
Object.defineProperty(
    STVariableReferenceError.prototype,
    'name',
    {
        value: 'STVariableReferenceError'
    }
);

const createInterpreterSemantics = (partContext, systemContext) => {
    return {
        Script: function(scriptParts, _){
            return scriptParts.interpret();
        },
        MessageHandler: function(handlerOpen, lineTerm, optionalStatementList, handlerClose){
            let {messageName, parameters} = handlerOpen.interpret();
            let handlerFunction = function(senders, ...args){

                // In the grammar, the StatementList is
                // an optional rule, meaning the result of the rule
                // is an empty array (no statementlist) or a single
                // item array (the statementlist)
                if(optionalStatementList.children.length == 0){
                    return;
                }
                let statementList = optionalStatementList.children[0];

                // Next, we initialize a new ActivationContext
                // that will hold all variable information for
                // the execution of this handler.
                // We push it to the top of the current execution stack
                // and set the argument variables to locals
                args.forEach((argValue, index) => {
                    let argName = parameters[index];
                    systemContext.executionStack.current.setLocal(
                        argName,
                        argValue
                    );
                });

                // Because StatementList is both optional *and* made up
                // of iterable StatementLine rules (ie, 'StatementLine+' in grammar),
                // we need to "unwrap" these nodes without calling interpret() on them.
                // This ensures that expressions within the statements, like variable lookups,
                // are not called before any preceding statements have been interpreted and
                // the corresponding messages have already been sent. For example, statement 1 might
                // set a variable that statement 2 needs to lookup and use, so we want the lookup to
                // occur after statement 1 has been interpreted and the message for it has
                // been sent.
                statementList.children.forEach(statementLines => {
                    statementLines.children.forEach(statementLine => {
                        let message = statementLine.interpret();
                    });
                });
            };

            partContext._commandHandlers[messageName] = handlerFunction;
        },

        MessageHandlerOpen: function(literalOn, messageName, optionalParameterList){
            // Because the ParameterList here is optional, if
            // it is set it will be in the form of a size 1 array.
            // This single array item will itself be an array of the
            // parameter variable names.
            // Otherwise, an empty array indicates no params
            // are passed in for this handler
            let params = optionalParameterList.interpret();
            if(params.length > 0){
                params = params[0];
            }
            return {
                messageName: messageName.sourceString,
                parameters: params
            };
        },

        ParameterList: function(parameterString){
            return parameterString.asIteration().children.map(child => {
                return child.sourceString;
            });
        },


        InClause: function(inLiteral, objectSpecifier){
            return objectSpecifier.interpret();
        },

        Command_answer: function(answer, expression){
            let msg = {
                type: "command",
                commandName: "answer",
                args: [
                    expression.interpret()
                ]
            };
            return msg;
        },

        Command_goToDirection: function(goToLiteral, nextPrevious, systemObject){
            let args = [];
            args.push(nextPrevious.sourceString);
            if (systemObject.sourceString){
                args.push(systemObject.sourceString);
            }

            let msg = {
                type: "command",
                commandName: "go to direction",
                args: args
            };
            return msg;
        },

        Command_goToByObjectSpecifier: function(goToLiteral, objectSpecifier){
            let args = [
                objectSpecifier.interpret() // id of the object
            ];

            let msg = {
                type: "command",
                commandName: "go to",
                args: args
            };
            return msg;
        },

        Command_goToWebsite: function(goToLiteral, websiteLiteral, url){
            let args = [
                url.interpret()
            ];

            let msg = {
                type: "command",
                commandName: "go to website",
                args: args
            };
            return msg;
        },

        Command_addProperty: function(addLiteral, propertyLiteral, propNameAsLiteral, toLiteral, systemObject){
            let specifiedObjectId = systemObject.interpret()[0] || null;
            let args = [
                propNameAsLiteral.interpret(), // The property name
                specifiedObjectId
            ];

            let msg = {
                type: "command",
                commandName: "newProperty",
                args: args
            };
            return msg;
        },

        Command_addModel: function(addLiteral, newPartType, optionalPartName){
            // here no owner has been provided so we assume it is the first possible one
            let parent = findFirstPossibleAncestor(partContext, newPartType.sourceString);
            let args = [
                newPartType.sourceString,
                parent.id
            ];
            let optionalName = optionalPartName.interpret();
            if(optionalName && optionalName.length){
                args.push(optionalName[0]);
            }

            let msg = {
                type: "command",
                commandName: "newModel",
                args: args
            };
            return msg;
        },

        Command_addModelTo: function(addLiteral, newPartType, optionalPartName, toLiteral, objectSpecifier){
            let args = [
                newPartType.sourceString, // The kind of part to add
                objectSpecifier.interpret() // id of the parent model part
            ];

            let optionalName = optionalPartName.interpret();
            if(optionalName && optionalName.length){
                args.push(optionalName[0]);
            }

            let msg = {
                type: "command",
                commandName: "newModel",
                args: args
            };
            return msg;
        },

        Command_putVariable: function(putLiteral, value, intoLiteral, globalLiteral, destination){
            let global = false;
            if(globalLiteral.sourceString){
                global = true;
            };
            let args = [
                value.interpret(),
                destination.sourceString,
                global
            ];
            let msg = {
                type: "command",
                commandName: 'putInto',
                args
            };
            return msg;
        },

        Command_deleteProperty: function(deleteLiteral, propertyLiteral, propNameAsLiteral, fromLiteral, systemObject){
            let specifiedObjectId = systemObject.interpret()[0] || null;
            let args = [
                propNameAsLiteral.interpret(), // The property name
                specifiedObjectId
            ];

            let msg = {
                type: "command",
                commandName: "deleteProperty",
                args: args
            };
            return msg;
        },

        Command_deleteModel: function(deleteLiteral, objectSpecifier){
            let args = [
                objectSpecifier.interpret() // id of the object
            ];

            let msg = {
                type: "command",
                commandName: "deleteModel",
                args: args
            };
            return msg;
        },

        Command_setProperty: function(setLiteral, propNameAsLiteral, toLiteral, literalOrVarName, optionalInClause){
            let specifiedObjectId = optionalInClause.interpret()[0] || null;
            let args = [
                propNameAsLiteral.interpret(), // The property name
                literalOrVarName.interpret(), // The value or a var representing the value
                specifiedObjectId
            ];

            let msg = {
                type: "command",
                commandName: "setProperty",
                args: args
            };
            return msg;
        },

        Command_setSelection: function(setLiteral, selectionLiteral, propNameAsLiteral, toLiteral, literalOrVarName, optionalInClause){
            let specifiedObjectId = optionalInClause.interpret()[0] || null;
            let args = [
                propNameAsLiteral.interpret(), // The property name
                literalOrVarName.interpret(), // The value or a var representing the value
                specifiedObjectId
            ];

            let msg = {
                type: "command",
                commandName: "setSelection",
                args: args
            };
            return msg;
        },

        Command_ask: function(askLiteral, question){
            return {
                type: "command",
                commandName: "ask",
                args: [ question.interpret() ]
            };
        },

        Command_tellCommand: function(tellLiteral, objectSpecifier, toLiteral, command){
            return {
                type: 'command',
                commandName: 'tell',
                args: [
                    objectSpecifier.interpret(),
                    command.interpret()
                ]
            };
        },

        Command_arbitraryCommand: function(commandName, optionalArgumentList){
            // Because the argument list is optional here, it will
            // be either an empty array (no arguments) or a size 1
            // array (which itself will contain an array of the arguments)
            let optionalArguments = optionalArgumentList.interpret();
            if(optionalArguments.length > 0){
                optionalArguments = optionalArguments[0];
            }

            return {
                type: "command",
                commandName: commandName.sourceString,
                args: optionalArguments
            };
        },

        CommandArgumentList: function(list){
            return list.asIteration().interpret();
        },

        StatementLine: function(statement, newline){
            let message = statement.interpret();

            // Some statements, like if-then controls
            // and repeat controls, do not result in
            // messages but return null.
            // We ignore these.
            if(message && typeof(message) !== 'string'){
                let commandResult = partContext.sendMessage(message, partContext);
                if(systemContext.executionStack.current){
                    systemContext.executionStack.current.setLocal('it', commandResult);
                }
                return null;
            } else {
                return message;
            }
        },

        Statement: function(actualStatement, optionalComment){
            return actualStatement.interpret();
        },

        Expression_addExpr: function(firstExpression, operation, secondExpression){
            let first = firstExpression.interpret();
            let second = secondExpression.interpret();
            return first + second;
        },

        Expression_minusExpr: function(firstExpr, operation, secondExpr){
            let first = firstExpr.interpret();
            let second = secondExpr.interpret();
            return first - second;
        },

        Expression_divideExpr: function(firstExpr, operation, secondExpr){
            let first = firstExpr.interpret();
            let second = secondExpr.interpret();
            return first / second;
        },

        Expression_moduloDivideExpr: function(firstExpr, operation, secondExpr){
            let first = firstExpr.interpret();
            let second = secondExpr.interpret();
            return first % second;
        },

        Expression_timesExpr: function(firstExpression, operation, secondExpression){
            let first = firstExpression.interpret();
            let second = secondExpression.interpret();
            return first * second;
        },

        Expression_stringConcatExpr: function(firstExpression, operation, secondExpression){
            // When we encounter the "&" operator, we coerce both expressions into
            // a string
            let first = firstExpression.interpret().toString();
            let second = secondExpression.interpret().toString();
            return `${first}${second}`;
        },

        Factor_parenFactor: function(leftParen, expression, rightParen){
            return expression.interpret();
        },

        Factor_notFactor: function(notLiteral, expression){
            return !expression.interpret();
        },

        EqualityConditional: function(expr1, comparatorLiteral, expr2){
            let first = expr1.interpret();
            let second = expr2.interpret();
            return first === second;
        },

        NonEqualityConditional: function(expr1, comparatorLiteral, expr2){
            let first = expr1.interpret();
            let second = expr2.interpret();
            return first !== second;
        },

        Conditional_gtComparison: function(expr1, gtLiteral, expr2){
            let first = expr1.interpret();
            let second = expr2.interpret();
            return first > second;
        },

        Conditional_ltComparison: function(expr1, ltLiteral, expr2){
            let first = expr1.interpret();
            let second = expr2.interpret();
            return first < second;
        },

        Conditional_gteComparison: function(expr1, gteLiteral, expr2){
            let first = expr1.interpret();
            let second = expr2.interpret();
            return first >= second;
        },

        Conditional_lteComparison: function(expr1, lteLiteral, expr2){
            let first = expr1.interpret();
            let second = expr2.interpret();
            return first <= second;
        },

        ThereIsAnObjectConditional: function(thereLiteral, isLiteral, aOrAnLiteral, objectSpecifier){
            try{
                objectSpecifier.interpret();
                return true;
            } catch(e){
                return false;
            };
        },

        ThereIsNotAnObjectConditional: function(thereLiteral, isLiteral, notLiteral, aOrAnLiteral, inClause){
            try{
                objectSpecifier.interpret();
                return false;
            } catch(e){
                return true;
            };
        },

        ThereIsAPropertyConditional_withSpecifier: function(thereLiteral, isLiteral, aLiteral, propertyLiteral, propName, ofLiteral, objectSpecifier){
            let targetId = objectSpecifier.interpret();
            let target = systemContext.partsById[targetId];
            if(!target){
                throw new Error(`Could not find part with id ${targetId} (${this.sourceString})`);
            }
            let property = target.partProperties.findPropertyNamed(propName.interpret());
            if(property){
                return true;
            }
            return false;
        },

        ThereIsAPropertyConditional_withoutSpecifier: function(thereLiteral, isLiteral, aLiteral, propertyLiteral, propName){
            let property = partContext.partProperties.findPropertyNamed(propName.interpret());
            if(property){
                return true;
            }
            return false;
        },

        ThereIsNotAPropertyConditional_withSpecifier: function(thereLiteral, isLiteral, notLiteral, aLiteral, propertyLiteral, propName, ofLiteral, objectSpecifier){
            let targetId = objectSpecifier.interpret();
            let target = systemContext.partsById[targetId];
            if(!target){
                throw new Error(`Could not find part with id ${targetId} (${this.sourceString})`);
            }
            let property = target.partProperties.findPropertyNamed(propName.interpret());
            if(property){
                return false;
            }
            return true;
        },

        ThereIsNotAPropertyConditional_withoutSpecifier: function(thereLiteral, isLiteral, notLiteral, aLiteral, propLiteral, propName){
            let property = partContext.partProperties.findPropertyNamed(propName.interpret());
            if(property){
                return false;
            }
            return true;
        },

        IfThenInline: function(ifLiteral, conditional, thenLiteral, statement, optionalComment){
            let shouldEvaluate = conditional.interpret();
            if(shouldEvaluate){
                return statement.interpret();
            } else {
                return null;
            }
        },

        IfThenSingleline_withoutElse: function(ifLine, lineTerm1, thenLine){
            let condition = ifLine.interpret();
            if(condition){
                return thenLine.interpret();
            } else {
                return null;
            }
        },

        IfThenSingleline_withElse: function(ifLine, lineTerm1, thenLine, lineTerm2, elseLine){
            let condition = ifLine.interpret();
            if(condition){
                return thenLine.interpret();
            } else {
                return elseLine.interpret();
            }
        },

        IfThenMultiline_withElse: function(ifLine, lineTerm, multiThen, multiElse, endIfLine){
            let condition = ifLine.interpret();
            if(condition){
                return multiThen.interpret();
            } else {
                return multiElse.interpret();
            }
        },

        IfThenMultiline_withoutElse: function(ifLine, lineTerm, multiThen, endIfLine){
            let condition = ifLine.interpret();
            if(condition){
                return multiThen.interpret();
            }
            return null;
        },

        IfLine: function(ifLiteral, conditional, optionalComment){
            return conditional.interpret();
        },

        ThenLine: function(thenLiteral, statement, optionalComment){
            return statement.interpret();
        },

        ElseLine: function(elseLiteral, statement, optionalComment){
            return statement.interpret();
        },

        ControlStatementLine: function(statementLine){
            return statementLine.interpret();
        },

        MultiThen: function(thenLiteral, optionalComment, lintTerm, controlStatementLines){
            return controlStatementLines.interpret();
        },

        MultiElse: function(elseLiteral, optionalComment, lineTerm, controlStatementLines){
            return controlStatementLines.interpret();
        },

        KindConditional: function(expr1, comparatorLiteral, expr2){
            // TODO: Flesh out this function to account for
            // various object types and their kind comparisons
            return false;
        },

        NotKindConditional: function(expr1, comparatorLiteral, expr2){
            // TODO: Flesh out this function to account for
            // various object types and their kind comparisons
            return true;
        },

        RepeatControlForm_forNumTimes: function(repeatLit, optionalForLit, intOrVar, timesLit){
            return {
                repeatType: 'forNumTimes',
                numTimes: intOrVar.interpret()
            };
        },

        RepeatControlForm_untilCondition: function(repeatLit, untilLit, conditional){
            return {
                repeatType: 'untilCondition',
                condition: conditional
            };
        },

        RepeatControlForm_whileCondition: function(repeatLit, whileLit, conditional){
            return {
                repeatType: 'whileCondition',
                condition: conditional
            };
        },

        RepeatControlForm_withStartFinish: function(repeatLit, withLit, varName, eqLit, firstVal, toLit, secondVal){
            return {
                repeatType: 'withStartFinish',
                varName: varName.sourceString,
                start: firstVal.interpret(),
                finish: secondVal.interpret()
            };
        },

        RepeatAdjust_exit: function(_){
            return 'exit repeat';
        },

        RepeatAdjust_next: function(_){
            return 'next repeat';
        },

        RepeatBlock: function(repeatControl, lineTerm, statementLineOrRepAdjustPlus, endLiteral){
            let repeatInfo = repeatControl.interpret();
            let statementLines = statementLineOrRepAdjustPlus.children;
            switch(repeatInfo.repeatType){
            case 'forNumTimes':
                for(let i = 1; i <= repeatInfo.numTimes; i++){
                    let shouldBreak = false;
                    let shouldPass = false;
                    for(let j = 0; j < statementLines.length; j++){
                        let currentStatement = statementLines[j];
                        let result = currentStatement.interpret();
                        if(result == 'exit repeat'){
                            shouldBreak = true;
                            break; // break out of this inner loop
                        } else if(result == 'next repeat'){
                            shouldPass = true;
                            break; // break out of this inner loop
                        }
                    }
                    if(shouldPass){
                        i += 1;
                    }
                    if(shouldBreak){
                        break; // break out of the main for loop
                    }
                }
                break; // Break out of the switch
            case 'untilCondition':
                let untilTestCondition = repeatInfo.condition.interpret();
                while(!untilTestCondition){
                    let shouldBreak = false;
                    for(let i = 0; i < statementLines.length; i++){
                        let currentStatement = statementLines[i];
                        let result = currentStatement.interpret();
                        if(result){
                            if(result == 'exit repeat'){
                                shouldBreak = true;
                                break;
                            } else if(result == 'next repeat'){
                                break;
                            }
                        }
                    }
                    if(shouldBreak){
                        break; // break out of the outer while loop
                    }
                    untilTestCondition = repeatInfo.condition.interpret();
                }
                break; // Break out of the switch case
            case 'whileCondition':
                let whileTestCondition = repeatInfo.condition.interpret();
                while(whileTestCondition){
                    let shouldBreak = false;
                    for(let i = 0; i < statementLines.length; i++){
                        let currentStatement = statementLines[i];
                        let result = currentStatement.interpret();
                        if(result == 'exit repeat'){
                            shouldBreak = true;
                            break; // break out of this inner loop
                        } else if(result == "next repeat"){
                            break; // break out of this inner loop
                        }
                    }
                    if(shouldBreak){
                        break; // break out of outer while loop (end repeat)
                    }
                    whileTestCondition = repeatInfo.condition.interpret();
                }
                break; // break out of switch case
            case 'withStartFinish':
                // For now, we assume that start is less than
                // finish. We should probably throw an error if
                // otherwise
                if(repeatInfo.start > repeatInfo.finish){
                    throw new Error(`Repeat error: start greater than finish`);
                }

                for(let i = repeatInfo.start; i <= repeatInfo.finish; i++){
                    systemContext.executionStack.current.setLocal(repeatInfo.varName, i);
                    let shouldBreak = false;
                    let shouldPass = false;
                    for(let j = 0; j < statementLines.length; j++){
                        let currentStatement = statementLines[j];
                        let result = currentStatement.interpret();
                        if(result == "exit repeat"){
                            shouldBreak = true;
                            break; // break out of this inner loop
                        } else if(result == "next repeat"){
                            shouldPass = true;
                            break; // break out of this inner loop
                        }
                    }
                    if(shouldPass){
                        i += 1;
                    }
                    if(shouldBreak){
                        break; // break out of the outer (repeat) loop
                    }
                }
            }
            return null;
        },

        PropertyValue_withSpecifier: function(theLiteral, propName, ofLiteral, objectSpecifier){
            let targetId = objectSpecifier.interpret();
            let target = systemContext.partsById[targetId];
            if(!target){
                throw new Error(`Could not find part with id ${targetId} (${this.sourceString})`);
            }
            return target.partProperties.getPropertyNamed(
                target,
                propName.interpret()
            );
        },

        PropertyValue_withoutSpecifier: function(theLiteral, propName){
            return partContext.partProperties.getPropertyNamed(
                partContext,
                propName.interpret()
            );
        },

        /** Object Specifiers **/


        /**
         * The partByTarget Partial Specifier
         * refers to partials that specify a part
         * specified in the "target" PartProperty
         * of the context part. The value of the
         * target property is any valid ObjectSpecifier
         * string.
         */
        PartialSpecifier_partByTarget(targetLiteral){
            return (context) => {
                let targetPropValue = context.partProperties.getPropertyNamed(context, "target");
                // use the partContext since the context object might not have any semantics set on it
                // For example, a context object/part which does not have a script which has been
                // compiled will not have had context._semantics set.
                let semantics = partContext._semantics;
                let matchObject = systemContext.grammar.match(targetPropValue, 'ObjectSpecifier');
                let targetId = semantics(matchObject).interpret();
                return systemContext.partsById[targetId];
            };
        },

        /**
         * The currentCard Partial Specifier
         * refers to partials that specify the current card
         * depending on the stack context.
         */
        PartialSpecifier_currentCard: function(currentLiteral, cardLiteral){
            return function(contextPart){
                return contextPart.currentCard;
            };
        },

        /**
         * The partByIndex Partial Specifier
         * refers to partials that specify a part
         * type and an integer literal, for ex:
         *     field 3
         * The above example refers to the third
         * field part in its owner/parent part.
         */
        PartialSpecifier_partByIndex: function(objectType, integerLiteral){
            let index = integerLiteral.interpret();
            if(index < 1){
                throw new Error(`Part indices must be 1 or greater`);
            }
            return function(contextPart){
                if(objectType.sourceString == 'part'){
                    if(index > contextPart.subparts.length){
                        throw new Error(`${contextPart.type}[${contextPart.id}] does not have a part numbered ${index}`);
                    }
                    return contextPart.subparts[index-1];
                } else {
                    let partsOfType = contextPart.subparts.filter(subpart => {
                        return subpart.type == objectType.sourceString;
                    });
                    if(index > partsOfType.length){
                        throw new Error(`${contextPart.type}[${contextPart.id}] does not have a ${objectType.sourceString} numbered ${index}`);
                    }
                    return partsOfType[index-1];
                }
            };
        },

        /**
         * The partByNumericalIndex Partial Specifier
         * refers to partial that specify a part
         * type preceded by the English word for the
         * number. For the moment we accept first - tenth
         * Example:
         *     sixth button
         */
        PartialSpecifier_partByNumericalIndex: function(numericalKeyword, objectType){
            let index = numericalKeyword.interpret();
            return function(contextPart){
                if(objectType.sourceString == 'part'){
                    if(index > contextPart.subparts.length){
                        throw new Error(`${contextPart.type}[${contextPart.id}] does not have a part numbered ${index}`);
                    }
                    if(index < 0){
                        // An index of -1 indicates the "last"
                        // item of the desired collection was
                        // specified
                        return contextPart.subparts[contextPart.subparts.length - 1];
                    } else {
                        return contextPart.subparts[index-1];
                    }
                } else {
                    let partsOfType = contextPart.subparts.filter(subpart => {
                        return subpart.type == objectType.sourceString;
                    });
                    if(index > partsOfType.length){
                        throw new Error(`${contextPart.type}[${contextPart.id}] does not have a ${objectType.sourceString} numbered ${index}`);
                    }
                    if(index < 0){
                        // An index of -1 indicates the "last"
                        // item of the desired collection was
                        // specified
                        return partsOfType[partsOfType.length - 1];
                    } else {
                        return partsOfType[index-1];
                    }
                }
            };
        },

        /**
         * The partByName Partial Specifier
         * refers to a partial that specifies a part
         * by its name property. Example:
         *     card "My Custom Card"
         */
        PartialSpecifier_partByName: function(objectType, stringLiteral){
            let name = stringLiteral.interpret();
            if(objectType.sourceString == 'part'){
                return function(contextPart){
                    let found = contextPart.subparts.filter(subpart => {
                        let foundName = subpart.partProperties.getPropertyNamed(
                            subpart,
                            'name'
                        );
                        return name == foundName;
                    });
                    if(found.length){
                        return found[0];
                    }
                    throw new Error(`${contextPart.type}[${contextPart.id}] does not have a part named "${name}"`);
                };
            } else {
                return function(contextPart){
                    let found = contextPart.subparts.filter(subpart => {
                        return subpart.type == objectType.sourceString;
                    }).filter(subpart => {
                        let foundName = subpart.partProperties.getPropertyNamed(
                            subpart,
                            'name'
                        );
                        return foundName == name;
                    });
                    if(found.length){
                        return found[0];
                    }
                    throw new Error(`${contextPart.type}[${contextPart.id}] does not have a ${objectType.sourceString} named "${name}"`);
                };
            }
        },

        /**
         * The 'this' specifier is a terminal (final)
         * specifier that refers to one of three things:
         * 1. the type of the current part executing the script,
         *    example: this button
         * 2. Card, which refers to the card that owns the
         *    part that is currently executing the script, ex:
         *    this card
         * 3. Stack, which refers to the stack that owns the
         *    part that is currently executing the script, ex:
         *    this stack
         */
        TerminalSpecifier_thisSystemObject: function(thisLiteral, systemObject){
            let targetType = systemObject.sourceString;
            return function(contextPart){
                if(targetType == partContext.type){
                    return partContext;
                } else {
                    return findNearestParentOfKind(partContext, targetType);
                }
            };
        },

        /**
         * The 'current' specifier is a terminal (final)
         * specifier that refers to either the current card or stack
         * being displayed to the user.
         * There are only two possible valid options:
         *     `current card`
         *     `current stack`
         */
        TerminalSpecifier_currentSystemObject: function(currentLiteral, systemObject){
            let targetType = systemObject.sourceString;
            return function(contextPart){
                if(targetType == 'stack'){
                    return systemContext.getCurrentStackModel();
                } else if(targetType == 'card'){
                    return systemContext.getCurrentCardModel();
                } else {
                    throw new Error(`${targetType} cannot be a 'current' system object`);
                }
            };
        },

        /**
         * The partById specifier is a terminal (final)
         * specifier that refers to a given part type
         * by its unique system id. For any kind of part,
         * we use `part id <objectId>`
         * Examples: `card id 266` `part id 5`
         */
        TerminalSpecifier_partById: function(objectType, idLiteral, objectId){
            let id = objectId.sourceString;
            let found = systemContext.partsById[id];
            if(!found){
                throw new Error(`Cannot find ${objectType.sourceString} with id ${objectId}`);
            }
            return function(context){
                return found;
            };
        },

        /**
         * A "prefixed" queried specifier is just
         * a PartialSpecifier with "of" in front of it, indicating
         * that a different partial will precede it be queried inside of it.
         * Example `of button "My Button"`
         */
        QueriedSpecifier_prefixed: function(partialSpecifier, ofLiteral){
            return partialSpecifier.interpret();
        },

        /**
         * A nested queried specifier is one that has two
         * or more prefixed specifiers. The simplest would be
         * something like:
         *     `of card "My Card" of stack "Another named stack"`
         */
        QueriedSpecifier_nested: function(firstQuery, secondQuery){
            return function(contextPart){
                let inner = secondQuery.interpret()(contextPart);
                let outer = firstQuery.interpret()(inner);
                return outer;
            };
        },

        /**
         * An ObjectSpecifier without an annotated
         * rule means it was interpreted as just
         * a TerminalSpecifier of some sort.
         * However, we need to extract the id
         * and return that result, since that is what is
         * expected of all interpreted ObjectSpecifiers
         */
        ObjectSpecifier_singleTerminal: function(terminalSpecifier){
            let found = terminalSpecifier.interpret()();
            return found.id;
        },

        /**
         * A Compound with terminal specifier is a QueriedSpecifier
         * that finishes with a Terminal specifier.
         * Example: `of button 3 of card "Some named card" of current stack`
         */
        ObjectSpecifier_compoundQueryWithTerminal: function(queriedSpecifier, terminalSpecifier){
            // The terminal here is the ultimate part context
            let finalPart = terminalSpecifier.interpret()();
            let result = queriedSpecifier.interpret()(finalPart);
            return result.id;
        },

        /**
         * A Compound without terminal specifier is a QueriedSpecifier
         * that finishes with a Partial specifier.
         * Example: `of button 3 of first card` (which can continue `..of current stack` etc)
         * `first button of first area of stack 3`
         * `first button of area two of stack 3`
         */
        ObjectSpecifier_compoundQueryWithoutTerminal: function(queriedSpecifier, partialSpecifier){
            // if the partialSpecfier refers to either area, card or stack
            // then go to its owner for the context
            // if it refers to the current card then find the owner for the context
            let children = partialSpecifier.children[0].children;
            let systemObjectString;
            if(children[0].sourceString == "current" && children[1].sourceString == "card"){
                systemObjectString = "card";
            } else {
                children.forEach((child) => {
                    if(child.ctorName == "systemObject"){
                        systemObjectString = child.sourceString;
                    }
                });
            }
            let finalPart = findFirstPossibleAncestor(partContext, systemObjectString);
            let finalPartial = partialSpecifier.interpret()(finalPart);
            let result = queriedSpecifier.interpret()(finalPartial);
            return result.id;
        },

        /**
         * A single non-terminal ObjectSpecifier is just a Partial
         * specifier by itself. When present outside of a QueriedSpecifier,
         * it will be interpreted in the current context and treated
         * as terminal/final. For example:
         *     button 4
         * by itself as a whole specifier will be interpreted as
         * `button 4 of this card`
         */
        ObjectSpecifier_singleNonTerminal: function(partialSpecifier){
            // A single non-terminal object specifier is one
            // whose terminal object is implicitly assumed to
            // be the card or the stack in which the current context part
            // exists.
            let children = partialSpecifier.children[0].children;
            let systemObjectString;
            if(children[0].sourceString == "current" && children[1].sourceString == "card"){
                return systemContext.getCurrentCardModel().id;
            } else {
                children.forEach((child) => {
                    if(child.sourceString == "part" || child.sourceString == "target" || child.ctorName == 'systemObject'){
                        systemObjectString = child.sourceString;
                    }
                });
            }
            // the systemObject is the target (defined in it's "target" part property), then we need to
            // first get the target property value (string) and interpret that
            if(systemObjectString == "target"){
                let targetPropValue = partContext.partProperties.getPropertyNamed(partContext, "target");
                let semantics = partContext._semantics;
                let matchObject = systemContext.grammar.match(targetPropValue, 'ObjectSpecifier');
                let targetId = semantics(matchObject).interpret();
                return targetId;
            } else if(systemObjectString == "current card"){
                systemObjectString = "card";
            }
            let finalPart = findFirstPossibleAncestor(partContext, systemObjectString);
            let result = partialSpecifier.interpret()(finalPart);
            return result.id;
        },

        ObjectSpecifier_singleTerminal: function(terminalSpecifier){
            let result = terminalSpecifier.interpret()(partContext);
            return result.id;
        },

        anyLiteral: function(theLiteral){
            return theLiteral.interpret();
        },

        stringLiteral: function(openQuote, text, closeQuote){
            return text.sourceString;
        },

        booleanLiteral: function(text){
            if(text.sourceString == 'true'){
                return true;
            }
            if(text.sourceString == 'false'){
                return false;
            }
            throw new Error(`Invalid boolean literal: ${text}`);
        },

        integerLiteral: function(negativeSign, integer){
            let int = parseInt(integer.sourceString);
            let hasNegative = (negativeSign.sourceString == "-");
            if(hasNegative){
                return -1 * int;
            }
            return int; 
        },

        floatLiteral: function(negativeSign, onesPlace, decimal, restPlace){
            let floatString = `${onesPlace.sourceString}.${restPlace.sourceString}`;
            let hasNegative = (negativeSign.sourceString == "-");
            let result = parseFloat(floatString);
            if(hasNegative){
                return -1 * result;
            }
            return result;
        },

        numericalKeyword: function(numeralName){
            switch(numeralName.sourceString){
            case 'first':
                return 1;
            case 'second':
                return 2;
            case 'third':
                return 3;
            case 'fourth':
                return 4;
            case 'fifth':
                return 5;
            case 'sixth':
                return 6;
            case 'seventh':
                return 7;
            case 'eighth':
                return 8;
            case 'ninth':
                return 9;
            case 'tenth':
                return 10;
            }

            return -1;
        },

        variableName: function(letterPlus, optionalDigits){
            // Lookup the variable in the part's
            // current execution context
            // If the variable is not a key on the object,
            // we throw an error: this means the variable has not yet
            // been defined but is being looked up.
            let value = systemContext.executionStack.current.get(this.sourceString);
            if(value == undefined){
                throw new STVariableReferenceError(
                    `Variable ${this.sourceString} has not been defined`);
            }
            return value;
        },

        comment: function(dashesLiteral, nonLineTerminatorChars){
            // Interpret doesn't do anything
            // with comments.
            return null;
        },

        _terminal(){

        }
    };
};


export {
    createInterpreterSemantics,
    createInterpreterSemantics as default
};


<|start_filename|>js/objects/views/StackView.js<|end_filename|>
/**
 * StackView
 * ----------------------------------------------
 * I am a Webcomponent (custom element) representing
 * the view of a Stack.
 * I take up the full width of the current viewport
 * when I am being displayed.
 * My child elements are BackgroundView and CardView
 */

import PartView from './PartView.js';
import Stack from '../parts/Stack.js';

// by default, stacks are hidden unless they're
// the current stack, or else they have the class
// window-stack (suggesting there's window part
// who wishes to display it)
const templateString = `<slot></slot>`;

class StackView extends PartView {
    constructor(){
        super();

        // Setup templating and shadow dom
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Halo settings. Cards don't want
        //a halo to open
        this.wantsHalo = false;

        // Handle current-ness prop change
        this.onPropChange('current', this.handleCurrentChange);

        // Bind methods
        this.handleCurrentChange = this.handleCurrentChange.bind(this);
    }

    afterModelSet(){
        // Do an initial setting of the
        // current card
        this.handleCurrentChange();
    }

    handleCurrentChange(){
        // The value of the current prop is the card ID
        // of the child Card that should be the
        // current one. We remove the current-card class from
        // the previous current card and add it to the new one.
        let currentCard = this.querySelector('.current-card');
        let nextCurrentId = this.model.partProperties.getPropertyNamed(
            this.model,
            'current'
        );
        let shouldNotify = false;
        let selector = `:scope > st-card[part-id="${nextCurrentId}"]`;
        if(this.isLensed){
            selector = `:scope > st-card[lens-part-id="${nextCurrentId}"]`;
            shouldNotify = true;
        }
        let nextCurrentCard = this.querySelector(selector);
        // if there is no currentCard and no next currentCard we set it to be the first
        // card child (this can happen when new ids are created on deserialization and so
        // the current property stored id is no longer relevant)
        if(!nextCurrentCard && !currentCard){
            nextCurrentCard = this.querySelector(`:scope > st-card`);
            // if there are no cards at all, this must be a brand new stack
            if(!nextCurrentCard){
                return;
            }
            this.model.partProperties.setPropertyNamed(
                this.model,
                "current",
                nextCurrentCard.id,
                shouldNotify
            );
        }
        if(nextCurrentCard){
            nextCurrentCard.classList.add('current-card');
        } else {
            return;
        }
        if(currentCard && currentCard != nextCurrentCard){
            currentCard.classList.remove('current-card');
        }
    }

    // override subclass methods
    newSubpartView(newView){
        if(this.childNodes.length && newView.name == "CardView"){
            let lastCardNode;
            this.childNodes.forEach((child) => {
                if(child.name == "CardView"){
                    lastCardNode = child;
                }
            });
            if(lastCardNode){
                // insert after the last card
                lastCardNode.after(newView);
            } else {
                // since there are no cards
                // insert before all children
                this.childNodes[0].insertBefore(newView);
            }
        } else {
            this.appendChild(newView);
        }
    }

};

export {
    StackView,
    StackView as default
};


<|start_filename|>css/core.css<|end_filename|>
/**
 * SimpleTalk Core CSS
 * ---------------------------------
 * This CSS file includes all "public" stlying for
 * the base PartViews, like WorldView, StackView, etc.
 * If you come across behavior not defined in this file,
 * be sure to check the Shadow DOM styling for the given
 * View components.
 **/

/* Global Variables */
:root {
    --marching-ant-width: 1px;
    --marching-ant-length: 10px;
    --marching-ant-color: black;
    --palette-yellow: #ffea95;
    --palette-blue: #004b67;
    --palette-green: #41ccb4;
    --palette-orange: #ff7c5d;
    --palette-red: #c70151;
    --palette-cornsik: #FFF8DC;
    --container-default-width: 20vw;
    --container-default-height: 15vh;
    --drawing-default-width: 500px;
    --drawing-default-height: 300px;
    --field-default-width: 300px;
    --field-default-height: 200px;
}

/* Keyframe Animations */
@keyframes marching-ants-animation {
    0%   {background-position: 0 0, var(--marching-ant-length) 100%, 0 var(--marching-ant-length), 100% 0;}
    100% {background-position: var(--marching-ant-length) 0, 0 100%, 0 0, 100% var(--marching-ant-length);}
}

/* Prerequisites */
* {
    visibility: initial;
    box-sizing: border-box;
}

body {
    display: block;
    position: relative;
    overflow: hidden;
    font-family: "Crimson Pro";
    background-image: /* tint image */
                      linear-gradient(to right, rgba(192, 192, 192, 0.85), rgba(192, 192, 192, 0.75)),
                      /* checkered effect */
                      linear-gradient(to right, black 50%, white 50%),
                      linear-gradient(to bottom, black 50%, white 50%);
    background-blend-mode: normal, difference, normal;
    background-size: 2em 2em;
}

.active {
    outline: solid var(--palette-orange) 1px;
}

/* =World Styles
 *----------------------------------------------------*/
st-world {
    display: block;
    position: absolute;
    overflow: hidden;
    width: 100vw;
    height: 100vh;
    box-shadow: 0px 0px 30px 10px rgba(100, 100, 100, 0.8);
}


/* =Stack Styles
 *----------------------------------------------------*/
st-stack {
    display: none;
    position: relative;
    width: 100%;
    height: 100%;
}

st-window st-stack {
    display: block;
}
.current-stack {
    display: block;
}

/* =Card Styles
 *----------------------------------------------------*/
st-card {
    display: none;
    position: relative;
    width: 100%;
    height: 100%;
}

.current-card {
    overflow: auto;
}

.current-card,
st-card[role="lens"] {
    display: block;
    visibility: initial;
}

/* =Button Styles
 *----------------------------------------------------*/
st-button {
    box-sizing: border-box;
    display: flex;
    position: absolute;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding-left: 10px;
    padding-right: 10px;
    padding-top: 5px;
    padding-bottom: 5px;
    user-select: none;
    margin-left: -1px;
    margin-right: -1px;
}

st-button:not(.editing):hover {
    cursor: pointer;
}

st-button:(.editing){
    background-color: white;
    color: black;
}

st-button:active{
    outline: solid var(--palette-orange) 1px;
}
/* =Window Styles
 *----------------------------------------------------*/
st-window {
    box-sizing: border-box;
    display: flex;
    position: absolute;
    flex-direction: column;
    border: 1px solid rgba(100, 100, 100, 0.2);
    box-shadow: 3px 4px 4px 0px rgba(50, 50, 50, 0.3);
    z-index: 100;
}

st-window:active {
    z-index: 100;
}

/* =Area Styles
 *---------------------------------------------------------*/
st-area {
    position: absolute;
    border: 1px solid rgb(150, 150, 150);
    width: var(--container-default-width);
    height: var(--container-default-height);
}

st-area:empty {
    display: block;
    position: absolute;
    border: 1px solid rgb(150, 150, 150);
    width: var(--container-default-width);
    height: var(--container-default-height);
    flex-shrink: 0;
}

st-area.outer-allow-scroll {
    overflow: hidden;
}
-----------------*/


/* =Svg Styles
 *---------------------------------------------------------*/
st-svg {
    display: block;
    position: absolute;
    /* border: 1px solid rgb(150, 150, 150);*/
}

/* =Drawing Styles
 *----------------------------------------------------*/
st-drawing {
    background-color: #1fe0; /* transparent */
    position: absolute;
}

/* =Field Styles
 *----------------------------------------------------*/
st-field {
    width: var(--field-default-width);
    height: var(--field-default-height);
    background-color: var(--palette-cornsik);
    position: absolute;
    display: block;
}

/* =Layout Class Styles
 * --------------------------------------------------------*/
.list-layout {
    display: flex;
    flex-direction: row;
}

st-world > st-stack > :not(.current-card).list-layout {
    display: none;
}

.list-column {
    flex-direction: column;
}

.list-layout:not(.list-column).list-align-top {
    align-items: start;
}

.list-layout:not(.list-column).list-align-bottom {
    align-items: end;
}

.list-layout.list-column.list-align-left {
    align-items: start;
}

.list-layout.list-column.list-align-right {
    align-items: end;
}

.list-layout.list-align-center {
    align-items: center;
}

.list-layout.list-distribution-center {
    justify-content: center;
}

.list-layout.list-distribution-start {
    justify-content: start;
}

.list-layout.list-distribution-end {
    justify-content: end;
}

.list-layout.list-distribution-space-between {
    justify-content: space-between;
}

.list-layout.list-distribution-space-around {
    justify-content: space-around;
}

.wrap-list {
    flex-wrap: wrap;
    align-content: start;
}

.list-layout > *:not(st-window):not(st-editor) {
    position: relative !important;
    top: auto !important;
    left: auto !important;
}

.list-layout > st-window,
.list-layout > st-editor {
    position: absolute !important;
}

.list-layout.list-column > .h-space-fill {
    width: 100% !important;
}

.list-layout:not(.list-column) > .v-space-fill {
    height: 100% !important;
}

.list-layout.list-column > .v-space-fill,
.list-layout:not(.list-column) > .h-space-fill {
    flex: 1;
    height: auto !important;
}

.list-layout:not(.list-column) > .h-rigid {
    flex: none;
}

.v-shrink-wrap {
    height: max-content !important;
}

.h-shrink-wrap {
    width: max-content !important;
}

.pin-top {
    top: 0 !important;
    bottom: unset !important;
}

.pin-bottom {
    bottom: 0 !important;
    top: unset !important;
}

.pin-left {
    left: 0 !important;
    right: unset !important;
}

.pin-right {
    right: 0 !important;
    left: unset !important;
}

/* =Layout Exceptions
 * --------------------------------------------------------*/
/* We don't want list-layout to display on Cards when they
 * are not current in the current stack (but we want this
 * in stack windows) */
/* st-stack.current-stack > st-card.list-layout:not(.current-card){ */
/*     display: none; */
/* } */
wrapped-view st-stack st-card:not(.current-card){
    display: none;
}

wrapped-view > st-card,
wrapped-view > st-stack {
    display: block;
}

/* =Editor
 * --------------------------------------------------------*/
editor-subparts > li {
    display: flex;
    flex-direction: row;
    align-items: baseline;
    width: 100%;
    border-bottom: 1px solid rgba(100, 100, 100, 0.0);
    transition: border 0.1s linear;
}

editor-subparts > li:hover {
    cursor: pointer;
    border-bottom: 1px solid rgba(100, 100, 100, 0.5);
    transition: border 0.1s linear;
}

editor-subparts > li > .icon-display-area {
    width: 1.5rem;
    height: 1.5rem;
    margin-bottom: 5px;
    align-self: center;
}

editor-subparts > li > .icon-display-area > svg {
    width: 100%;
    height: 100%;
}

editor-subparts > li > * {
    margin-right: 16px;
}

editor-subparts .id-span {
    font-family: monospace;
    color: rgba(0, 0, 0, 0.4);
}

editor-subparts button.add-part-button {
    outline: none;
    border: 1px solid rgba(0, 0, 0, 0.4);
    border-radius: 100%;
    background-color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.7rem;
    height: 1.7rem;
    padding: 4px;
}

editor-subparts button.add-part-button:hover {
    background-color: rgba(240, 240, 240);
    cursor: pointer;
}

editor-subparts button.add-part-button:active {
    background-color: rgba(220, 220, 220);
}

editor-subparts button.add-part-button > svg {
    width: 100%;
    height: 100%;
}


/* =Utility Classes
 * --------------------------------------------------------*/
.editing {
    background-image: linear-gradient(90deg, var(--marching-ant-color) 50%, transparent 50%),
        linear-gradient(90deg, var(--marching-ant-color) 50%, transparent 50%),
        linear-gradient(0, var(--marching-ant-color) 50%, transparent 50%),
        linear-gradient(0, var(--marching-ant-color) 50%, transparent 50%);
    background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
    background-size: var(--marching-ant-length) var(--marching-ant-width), var(--marching-ant-length) var(--marching-ant-width), var(--marching-ant-width) var(--marching-ant-length), var(--marching-ant-width) var(--marching-ant-length);
    animation: marching-ants-animation 400ms infinite linear;
    border-color: transparent;
}

.editable {
    outline: solid 1px var(--palette-orange);
}

.highlight {
    background-color: green!important;
}

.targeting:after {
    content: " ";
    display: block;
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
}

body.targeting-mode,
body.targeting-mode [part-id] {
    cursor: crosshair !important;
}

/* =Contextual Menu
 *---------------------------------------------------*/
.st-context-menu {
    display: flex;
    flex-direction: column;
    position: absolute;
    border: 1px solid black;
    background-color: white;
    box-shadow: 1px 2px 10px rgba(50, 50, 50, 0.7);
    z-index: 10000;
    padding-bottom: 8px;
    min-width: 200px;
    font-family: 'Helvetica', sans-serif;
}

.context-menu-item {
    display: block;
    padding-left: 16px;
    padding-right: 16px;
    padding-top: 4px;
    padding-bottom: 4px;
}

.context-menu-item:hover {
    cursor: pointer;
    background-color: rgba(50, 50, 50, 0.9);
    color: white;
}

.context-menu-item:hover > st-context-menu {
    background-color: white;
    color: black;
}

.context-menu-spacer {
    display: block;
    height: 1px;
    margin-left: 24px;
    margin-top: 8px;
    margin-bottom: 8px;
    margin-right: 24px;
    background-color: rgba(150, 150, 150, 0.7);
}


/* Navigator 
 * --------------------------------------------------*/

wrapped-view {
    display: block;
    box-sizing: border-box;
    width: 150px;
    height: 100px;
    border: 1px solid rgba(100, 100, 100, 0.4);
    overflow: hidden;
    margin-left: 15px;
    flex-shrink: 0;
    transition: transform 0.2s ease-in, opacity 0.2s ease-in, box-shadow 0.2s linear;
    transform: translateY(0);
    opacity: 1.0;
}

wrapped-view(:hover){
    cursor: pointer;
}

wrapped-view(:hover:not(.current)){
    border: 1px solid rgba(100, 100, 100, 0.8);
    box-shadow: 1px 1px 20px 1px rgba(150, 150, 150, 0.5);
    transition: box-shadow 0.1s linear, border 0.1s linear;
}

wrapped-view(:not(.current)) > st-card {
    display: none;
}

wrapped-view(.current){
    box-shadow: 1px 1px 20px 2px rgba(100, 100, 100, 0.8);
}
wrapped-view(:not(.current)){
    opacity: 0.5;
    transition: transform 0.2s ease-out, opacity 0.2s ease-out, box-shadow 0.2s linear;
}

wrapped-view(:not(.current)) > #number-display {
    opacity: 0.8;
    background-color: rgba(200, 200, 200, 0.5);
    transition: opacity 0.2s ease-out;
    z-index: 1000;
}

wrapped-view(:not(.current)) > #number-display > span {
    transform: translateY(0px);
    transition: transform 0.2s linear;
}

wrapped-view(:not(.current)) > #number-display {
    opacity: 0.8;
    background-color: rgba(200, 200, 200, 0.5);
    transition: opacity 0.2s ease-out;
    z-index: 1000;
}

wrapped-view(:not(.current)) > #number-display > span {
    transform: translateY(0px);
    transition: transform 0.2s linear;
}


<|start_filename|>js/objects/views/CardView.js<|end_filename|>
/**
 * CardView
 * --------------------------------------------------
 * I am a webcomponent representation of a Card.
 */

import PartView from './PartView.js';

const templateString = `
                <style>
                </style>
                <slot></slot>
`;

class CardView extends PartView {
    constructor(){
        super();

        // Setup template and shadow root
        const template = document.createElement('template');
        template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // Halo settings. Cards don't want
        //a halo to open
        this.wantsHalo = false;

        // Bind component methods
    }

    afterConnected(){
    }

    afterDisconnected(){
    }

    // override the default class method
    onClick(event){
        if(event.button == 0 && event.shiftKey){
            event.preventDefault();
            event.stopPropagation();
        }
    }

    addContextMenuItems(contextMenu){
        contextMenu.addSpacer();
        // Toolbox toggle hide/unhide
        let currentStack = window.System.getCurrentStackModel();
        let toolbox = currentStack.subparts.filter((part) => {
            let name = part.partProperties.getPropertyNamed(part, "name");
            return name == "Toolbox";
        })[0];
        // if there is no toolbox at all, that's weird but don't do anything
        if(toolbox){
            let hidden = toolbox.partProperties.getPropertyNamed(toolbox, "hide");
            if(hidden){
                contextMenu.addListItem(
                    "Unhide Toolbox",
                    (event) => {
                        toolbox.partProperties.setPropertyNamed(toolbox, "hide", false);
                    }
                );
            } else {
                contextMenu.addListItem(
                    "Hide Toolbox",
                    (event) => {
                        toolbox.partProperties.setPropertyNamed(toolbox, "hide", true);
                    }
                );
            }
        }
    }
};

export {
    CardView,
    CardView as default
};


<|start_filename|>js/objects/views/navigator/StackRow.js<|end_filename|>
/**
 * Navigator Stack Row
 * --------------------------------------------
 * I am a view on the WorldStack that shows each
 * subpart stack item as a wrapped lens view along
 * a row.
 */
import PartView from '../PartView.js';

const templateString = `
<style>
    :host {
        display: flex;
        position: relative;
        align-items: center;
        justify-content: flex-start;
        flex: 1;
    }
</style>
<slot name="stacks"></slot>
`;

class StackRow extends PartView {
    constructor(){
        super();
        this.wantsHalo = false;

        // Set up template
        this.template = document.createElement('template');
        this.template.innerHTML = templateString;
        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            this.template.content.cloneNode(true)
        );

        // Bound methods
        this.initView = this.initView.bind(this);
        this.addWrappedStack = this.addWrappedStack.bind(this);
        this.handleCurrentChange = this.handleCurrentChange.bind(this);
        this.handlePartAdded = this.handlePartAdded.bind(this);
        this.handlePartRemoved = this.handlePartRemoved.bind(this);
        this.showInitially = this.showInitially.bind(this);
        this.onWrapperClick = this.onWrapperClick.bind(this);
    }

    afterConnected(){
        // we don't want the context menu to open in the nav since
        // it doens't make sense atm and will error
        this.removeEventListener('contextmenu', this.onContextMenuClick);
    }


    afterModelSet(){
        this.removeAttribute('part-id');
        this.setAttribute('stack-id', this.model.id);
        this.onPropChange('current', this.handleCurrentChange);

        // Find the World Model's main view element.
        // We add the st-view-added CustomEvent listener
        // here so we can react only to direct stack additions
        // to the WorldStack (and not, say, to Windows or other nested kinds)
        let worldView = document.querySelector('st-world');
        worldView.addEventListener('st-view-added', this.handlePartAdded);
        worldView.addEventListener('st-view-removed', this.handlePartRemoved);
    }

    afterModelUnset(){
        let worldView = document.querySelector('st-world');
        worldView.removeEventListener('st-view-added', this.handlePartAdded);
        worldView.removeEventListener('st-view-removed', this.handlePartRemoved);
    }

    handleCurrentChange(){
        let currentId = this.model.currentStack.id;
        let wrappedViews = Array.from(
            this.querySelectorAll('wrapped-view')
        );
        wrappedViews.forEach(wrapper => {
            let wrappedId = wrapper.getAttribute('wrapped-id');
            if(wrappedId == this.model.currentStack.id.toString()){
                wrapper.classList.add('current');
            } else {
                wrapper.classList.remove('current');
            }
        });
    }

    handlePartAdded(event){
        // This handler is for the st-view-added
        // CustomEvent that is triggered by System when
        // newModel() has completed.
        if(event.detail.partType == 'stack'){
            let stackPart = window.System.partsById[event.detail.partId];
            this.addWrappedStack(stackPart);
            this.showInitially();
        }
    }

    handlePartRemoved(event){
        if(event.detail.partType == 'stack'){
            let wrappedView = this.querySelector(`wrapped-view[wrapped-id="${event.detail.partId}"]`);
            if(wrappedView){
                wrappedView.remove();
            }

            // Update numbers of remaining wrapped views in this StackRow
            Array.from(this.querySelectorAll('wrapped-view')).forEach(wrapper => {
                wrapper.updateNumberDisplay();
            });
        }
    }

    onWrapperClick(event){
        let wrapperIsCurrent = event.target.classList.contains('current');
        if(this.model && !wrapperIsCurrent){
            this.model.goToStackById(
                event.target.getAttribute('wrapped-id')
            );
        }
    }

    initView(){
        // Remove any existing wrapped views
        this.innerHTML = "";

        // We iterate over each corresponding Stack and:
        // * Create a clone of its view node;
        // * Attach the correct model;
        // * Set it to be a lensed view
        // * Do the same for all children, recursively
        this.model.subparts.filter(subpart => {
            return subpart.type == 'stack';
        }).forEach(stackPart => {
            this.addWrappedStack(stackPart);
        });

        // Setup the initial current-ness display
        this.handleCurrentChange();
    }

    showInitially(){
        // Nothing for now
    }

    addWrappedStack(aStack){  
        // Insert the lensed StackView into the wrapper
        let wrapper = document.createElement('wrapped-view');
        wrapper.setAttribute("slot", "stacks");
        wrapper.addEventListener('click', this.onWrapperClick);
        this.appendChild(wrapper);
        wrapper.setModel(aStack);
    }

    subpartOrderChanged(id, currentIndex, newIndex){
        let subpartNode = this.childNodes[currentIndex];
        if(!subpartNode){
            // this could be a model subpart which is not a stack and hence not
            // displayed in the StackRow
            return;
        }
        if(newIndex == this.childNodes.length - 1){
            this.appendChild(subpartNode);
        } else {
            // we need to account for whether the index of this
            // is before or after the newIndex
            if(currentIndex < newIndex){
                newIndex = newIndex + 1;
            }
            let referenceNode = this.childNodes[newIndex];
            this.insertBefore(subpartNode, referenceNode);
        }
        // Update number display of all wrapped views in the row
        Array.from(this.querySelectorAll(`wrapped-view`)).forEach(wrapper => {
            wrapper.updateNumberDisplay();
        });
    }
};

export {
    StackRow,
    StackRow as default
};


<|start_filename|>js/objects/tests/test-worldcatalog-with-preload.js<|end_filename|>
/**
 * System WorldCatalog Window Tests
 * ------------------------------------
 * These are integration tests that ensure
 * we are creating, propertly opening, and
 * have the correct functionality in the
 * World Catalog window when we send a message
 * to the System that it should be created.
 */
import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;

describe.skip('Opening a WorldCatalog', () => {
    describe('Sending openWorldCatalog message', () => {
        /**
         * this test is currently being skipped due to the
         * node-fetch "relative urls" issue.
         * The bug it produces is only an artifact of the testing
         * environment and does not appear in the live system
         **/
        it.skip('Sending #openWorldCatalog without arguments does not produce an error', () => {
            let openMsg = {
                type: 'command',
                commandName: 'openWorldCatalog',
                args: []
            };

            let currentCard = window.System.getCurrentCardModel();
            assert.exists(currentCard);

            let sendFunction = function(){
                currentCard.sendMessage(
                    openMsg,
                    currentCard
                );
            };
            expect(sendFunction).to.not.throw();
        });
    });
});


<|start_filename|>js/objects/parts/Browser.js<|end_filename|>
import {Part} from './Part.js';
import {
    addBasicStyleProps,
    addPositioningStyleProps,
    addTextStyleProps
} from '../utils/styleProperties.js';

class Browser extends Part {
    constructor(owner, src) {
        super(owner);

        // Properties
        this.partProperties.newBasicProp(
            "src",
            null
        );

        this.src = null;
        this.partProperties.newBasicProp(
            'readyState',
            "HAVE_NOTHING"
        );

        this.partProperties.newBasicProp(
            "play",
            false
        );

        this.partProperties.newBasicProp(
            "stop",
            null
        );

        // Private command handlers
        this.setPrivateCommandHandler("setURLTo", this.setURL);
        this.setPrivateCommandHandler("forward", this.sendMessageToBrowser);

        // Bind component methods
        this.setURL = this.setURL.bind(this);
        this.sendMessageToBrowser = this.sendMessageToBrowser.bind(this);


        // load the src if provided
        if(src){
            this.partProperties.setPropertyNamed(this, "src", url);
        }
        // Style properties
        addBasicStyleProps(this);
        addPositioningStyleProps(this);
        this.setupStyleProperties();
        this.partProperties.setPropertyNamed(
            this,
            'background-transparency',
            0,
            true, // notify
            true // set default
        );
        ["right", "left", "top", "bottom"].forEach((side) => {
            this.partProperties.setPropertyNamed(
                this,
                `border-${side}-width`,
                5,
                true, // notify
                true // set default
            );
        });
        this.partProperties.setPropertyNamed(
            this,
            "width",
            400,
            true, // notify
            true // set default
        );
    }

    get type(){
        return 'browser';
    }

    setURL(senders, sourceUrl){
        this.partProperties.setPropertyNamed(this, "src", sourceUrl);
    }

    sendMessageToBrowser(senders, message){
        let views = window.System.findViewsById(this.id);
        views.forEach((v) => {
            let iframe = v._shadowRoot.querySelector("iframe");
            iframe.contentWindow.postMessage(message, window.origin);
        });
    }
};

export {
    Browser,
    Browser as default
};


<|start_filename|>js/objects/views/WorldView.js<|end_filename|>
/**
 * WorldView
 * ---------------------------------------------
 * I am a Webcomponent (custom element) that represents
 * a view of a WorldStack model.
 * My element children should contain a single StackView representing
 * the current displayed stack (this comes from the model).
 * I am the root-level element for the SimpleTalk system in a web
 * page. There should only be one of me on any given HTML page.
 */
import PartView from './PartView.js';

const templateString = `<slot></slot>`;

const NODES_TO_IGNORE_WHEN_ARROW_KEY = [
    'TEXTAREA',
    'INPUT',
    'ST-FIELD'
];

class WorldView extends PartView {
    constructor(){
        super();

        // Set up templating and shadow dom
        // TODO: Put the template definition in this
        // module as formatted text
        const template = document.createElement('template');
        template.innerHTML = templateString;

        this._shadowRoot = this.attachShadow({mode: 'open'});
        this._shadowRoot.appendChild(
            template.content.cloneNode(true)
        );

        // The world never wants a halo
        this.wantsHalo = false;

        // Bound methods
        this.updateCurrentStack = this.updateCurrentStack.bind(this);
        this.receiveMessage = this.receiveMessage.bind(this);
        this.setupPropHandlers = this.setupPropHandlers.bind(this);
        this.handleKeyDown = this.handleKeyDown.bind(this);
        this.toggleNavigator = this.toggleNavigator.bind(this);

        // Setup prop handlers
        this.setupPropHandlers();
    }

    setupPropHandlers(){
        this.onPropChange('current', this.updateCurrentStack);
        this.onPropChange('navigator-open', this.toggleNavigator);
    }

    afterConnected(){
        document.addEventListener('keydown', this.handleKeyDown);
    }

    afterDisconnected(){
        document.removeEventListener('keydown', this.handleKeyDown);
    }

    afterModelSet(){
        // Do an initial update to display
        // the model's current stack
        this.updateCurrentStack();
        let navOpen = this.model.partProperties.getPropertyNamed(this.model, 'navigator-open');
        if(navOpen){
            this.toggleNavigator(true);
        }
    }

    updateCurrentStack(){
        // The value of the current prop is the stack ID
        // of the child Stack that should be the
        // current one. We remove the current-stack class from
        // the previous current stack and add it to the new one.
        let currentStack = this.querySelector('.current-stack');
        let nextCurrentId = this.model.partProperties.getPropertyNamed(
            this.model,
            'current'
        );
        let nextCurrentStack = this.querySelector(`:scope > st-stack[part-id="${nextCurrentId}"]`);
        if(nextCurrentStack){
            nextCurrentStack.classList.add('current-stack');
        } else {
            return;
        }
        // To prevent the setting of the same id as the current stack make sure
        // next and current are not the same
        if(currentStack && currentStack != nextCurrentStack){
            currentStack.classList.remove('current-stack');
        }
    }

    handleKeyDown(event){
        if(event.altKey && event.ctrlKey && event.code == "Space"){
            let navigator = document.querySelector('st-navigator');
            navigator.toggle();
        } else {
            // Bind arrow key events if and only if
            // the focus is not in any kind of text input.
            // We send the arrowKey command to the current card
            if(event.code.startsWith('Arrow')){
                if(!NODES_TO_IGNORE_WHEN_ARROW_KEY.includes(document.activeElement.nodeName)){
                    this.model.sendMessage({
                        type: 'command',
                        commandName: 'arrowKey',
                        args: [
                            // First arg is the direction
                            event.code.split('Arrow')[1].toLowerCase(),
                            event.ctrlKey,
                            event.altKey,
                            event.shiftKey
                        ],
                        shouldIgnore: true
                    }, this.model.currentStack.currentCard);
                }
            }
        }
    }

    // override subclass methods
    newSubpartView(newView){
        if(this.childNodes.length && newView.name == "StackView"){
            let lastStackNode;
            this.childNodes.forEach((child) => {
                if(child.name == "StackView"){
                    lastStackNode = child;
                }
            });
            if(lastStackNode){
                // insert after the last stack
                lastStackNode.after(newView);
            } else {
                // since there are no stacks
                // insert before all children
                this.childNodes[0].insertBefore(newView);
            }
        } else {
            this.appendChild(newView);
        }
    }

    // Navigator
    toggleNavigator(val){
        let nav = document.querySelector('st-navigator');
        if(val){
            nav.open();
        } else {
            nav.close();
        }
    }

};

export {
    WorldView,
    WorldView as default
};
