<commit_before>// @flow

import { ENUM, PALETTE } from "constants/controlTypes";
import { nearest } from "palettes";

import type { ColorRGBA, Palette } from "types";

import {
  cloneCanvas,
  fillBufferPixel,
  getBufferIndex,
  rgba,
  scaleMatrix
} from "utils";

export const BAYER_2X2 = "BAYER_2X2";
export const BAYER_3X3 = "BAYER_3X3";
export const BAYER_4X4 = "BAYER_4X4";
export const BAYER_8X8 = "BAYER_8X8";
export const SQUARE_5X5 = "SQUARE_5X5";
export const CORNER_4X4 = "CORNER_4X4";
export const BLOCK_VERTICAL_4X4 = "BLOCK_VERTICAL_4X4";
export const BLOCK_HORIZONTAL_4X4 = "BLOCK_HORIZONTAL_4X4";
export const HATCH_2X2 = "HATCH_2X2";
export const HATCH_3X3 = "HATCH_3X3";
export const HATCH_4X4 = "HATCH_4X4";
export const ALTERNATE_3X3 = "ALTERNATE_3X3";
export const DISPERSED_DOT_3X3 = "DISPERSED_DOT_3X3";

export type Threshold =
  | "BAYER_2X2"
  | "BAYER_3X3"
  | "BAYER_4X4"
  | "BAYER_8X8"
  | "SQUARE_5X5"
  | "DISPERSED_DOT_3X3"
  | "CORNER_4X4"
  | "BLOCK_VERTICAL_4X4"
  | "BLOCK_HORIZONTAL_4X4"
  | "HATCH_2X2"
  | "HATCH_3X3"
  | "HATCH_4X4"
  | "ALTERNATE_3X3";

// map[y][x]
const thresholdMaps: {
  [Threshold]: { width: number, thresholdMap: Array<Array<?number>> }
} = {
  [BAYER_2X2]: {
    width: 2,
    thresholdMap: scaleMatrix([[0, 2], [3, 1]], 1 / 4)
  },
  [BAYER_3X3]: {
    width: 3,
    thresholdMap: scaleMatrix([[0, 7, 3], [6, 5, 2], [4, 1, 8]], 1 / 9)
  },
  [BAYER_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]],
      1 / 16
    )
  },
  [BAYER_8X8]: {
    width: 8,
    thresholdMap: scaleMatrix(
      [
        [0, 48, 12, 60, 3, 51, 15, 63],
        [32, 16, 44, 28, 35, 19, 47, 31],
        [8, 56, 4, 52, 11, 59, 7, 55],
        [40, 24, 36, 20, 43, 27, 39, 23],
        [2, 50, 14, 62, 1, 49, 13, 61],
        [34, 18, 46, 30, 33, 17, 45, 29],
        [10, 58, 6, 54, 9, 57, 5, 53],
        [42, 26, 38, 22, 41, 25, 37, 21]
      ],
      1 / 64
    )
  },
  [SQUARE_5X5]: {
    width: 5,
    thresholdMap: scaleMatrix(
      [
        [40, 60, 150, 90, 10],
        [80, 170, 240, 200, 110],
        [140, 210, 250, 220, 130],
        [120, 190, 230, 180, 70],
        [20, 100, 160, 50, 30]
      ],
      1 / 255
    )
  },
  [DISPERSED_DOT_3X3]: {
    width: 3,
    thresholdMap: scaleMatrix([[0, 6, 3], [4, 7, 2], [5, 1, 8]], 1 / 9)
  },
  [CORNER_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 2, 5, 9], [1, 4, 8, 12], [3, 7, 11, 14], [6, 10, 13, 15]],
      1 / 16
    )
  },
  [BLOCK_VERTICAL_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]],
      1 / 4
    ),
    levels: 4
  },
  [BLOCK_HORIZONTAL_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 0, 0, 0], [1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]],
      1 / 4
    ),
    levels: 4
  },
  [HATCH_2X2]: {
    width: 2,
    thresholdMap: scaleMatrix([[0, 1], [1, 0]], 1 / 2),
    levels: 2
  },
  [HATCH_3X3]: {
    width: 3,
    thresholdMap: scaleMatrix([[0, 1, 2], [1, 2, 1], [2, 1, 0]], 1 / 3),
    levels: 3
  },
  [HATCH_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 1, 2, 3], [1, 2, 3, 2], [2, 3, 2, 1], [3, 2, 1, 0]],
      1 / 4
    ),
    levels: 4
  },
  [ALTERNATE_3X3]: {
    width: 3,
    thresholdMap: scaleMatrix([[0, 5, 1], [6, 2, 7], [3, 8, 4]], 1 / 9),
    levels: 9
  }
};

const getOrderedColor = (
  color: ColorRGBA,
  levels: number,
  tx: number,
  ty: number,
  threshold: Array<Array<?number>>
): ColorRGBA => {
  const thresholdValue = threshold[ty][tx];

  if (thresholdValue == null) {
    return rgba(255, 255, 0, 255); // error colour
  }

  const step = 255 / (levels - 1);

  // $FlowFixMe
  return color.map((c, i) => {
    if (i === 3) return c; // alpha channel
    const newColor = c + step * (thresholdValue - 0.5);
    const bucket = Math.round(newColor / step);
    return Math.round(bucket * step);
  });
};

export const optionTypes = {
  thresholdMap: {
    type: ENUM,
    options: [
      {
        name: "Bayer 2×2",
        value: BAYER_2X2
      },
      {
        name: "Bayer 3×3",
        value: BAYER_3X3
      },
      {
        name: "Bayer 4×4",
        value: BAYER_4X4
      },
      {
        name: "Bayer 8×8",
        value: BAYER_8X8
      },
      {
        name: "Dispersed Dot 3×3",
        value: DISPERSED_DOT_3X3
      },
      {
        name: "Digital Halftone 5×8",
        value: SQUARE_5X5
      },
      {
        name: "Corner 4×4",
        value: CORNER_4X4
      },
      {
        name: "Block Vertical 4×4",
        value: BLOCK_VERTICAL_4X4
      },
      {
        name: "Block Horizontal 4×4",
        value: BLOCK_HORIZONTAL_4X4
      },
      {
        name: "Hatch 2×2",
        value: HATCH_2X2
      },
      {
        name: "Hatch 3×3",
        value: HATCH_3X3
      },
      {
        name: "Hatch 4×4",
        value: HATCH_4X4
      },
      {
        name: "Alternate 3×3",
        value: ALTERNATE_3X3
      }
    ],
    default: BAYER_4X4
  },
  palette: { type: PALETTE, default: nearest }
};

const defaults = {
  thresholdMap: optionTypes.thresholdMap.default,
  palette: { ...optionTypes.palette.default, options: { levels: 2 } }
};

const ordered = (
  input: HTMLCanvasElement,
  options: {
    thresholdMap: Threshold,
    palette: Palette
  } = defaults
): HTMLCanvasElement => {
  const { palette, thresholdMap } = options;
  const levels =
    thresholdMap.levels || thresholdMap.length > 0
      ? thresholdMap.length * thresholdMap[0].length
      : 4;

  const output = cloneCanvas(input, false);

  const inputCtx = input.getContext("2d");
  const outputCtx = output.getContext("2d");

  if (!inputCtx || !outputCtx) {
    return input;
  }

  const buf = inputCtx.getImageData(0, 0, input.width, input.height).data;

  const threshold = thresholdMaps[thresholdMap];

  for (let x = 0; x < input.width; x += 1) {
    for (let y = 0; y < input.height; y += 1) {
      const tix = x % threshold.width;
      const tiy = y % threshold.width;
      const i = getBufferIndex(x, y, input.width);

      // Ignore alpha channel when calculating error
      const pixel = rgba(buf[i], buf[i + 1], buf[i + 2], buf[i + 3]);
      const orderedColor = getOrderedColor(
        pixel,
        levels,
        tix,
        tiy,
        threshold.thresholdMap
      );
      const color = palette.getColor(orderedColor, palette.options);

      fillBufferPixel(buf, i, color[0], color[1], color[2], buf[i + 3]);
    }
  }

  outputCtx.putImageData(new ImageData(buf, output.width, output.height), 0, 0);
  return output;
};

export default {
  name: "Ordered",
  func: ordered,
  options: defaults,
  optionTypes,
  defaults
};
<commit_msg>Ordered: Add BAYER_16X16<commit_after>// @flow

import { ENUM, PALETTE } from "constants/controlTypes";
import { nearest } from "palettes";

import type { ColorRGBA, Palette } from "types";

import {
  cloneCanvas,
  fillBufferPixel,
  getBufferIndex,
  rgba,
  scaleMatrix
} from "utils";

export const BAYER_2X2 = "BAYER_2X2";
export const BAYER_3X3 = "BAYER_3X3";
export const BAYER_4X4 = "BAYER_4X4";
export const BAYER_8X8 = "BAYER_8X8";
export const BAYER_16X16 = "BAYER_16X16";
export const SQUARE_5X5 = "SQUARE_5X5";
export const CORNER_4X4 = "CORNER_4X4";
export const BLOCK_VERTICAL_4X4 = "BLOCK_VERTICAL_4X4";
export const BLOCK_HORIZONTAL_4X4 = "BLOCK_HORIZONTAL_4X4";
export const HATCH_2X2 = "HATCH_2X2";
export const HATCH_3X3 = "HATCH_3X3";
export const HATCH_4X4 = "HATCH_4X4";
export const ALTERNATE_3X3 = "ALTERNATE_3X3";
export const DISPERSED_DOT_3X3 = "DISPERSED_DOT_3X3";

export type Threshold =
  | "BAYER_2X2"
  | "BAYER_3X3"
  | "BAYER_4X4"
  | "BAYER_8X8"
  | "BAYER_16X16"
  | "SQUARE_5X5"
  | "DISPERSED_DOT_3X3"
  | "CORNER_4X4"
  | "BLOCK_VERTICAL_4X4"
  | "BLOCK_HORIZONTAL_4X4"
  | "HATCH_2X2"
  | "HATCH_3X3"
  | "HATCH_4X4"
  | "ALTERNATE_3X3";

// map[y][x]
const thresholdMaps: {
  [Threshold]: { width: number, thresholdMap: Array<Array<?number>> }
} = {
  [BAYER_2X2]: {
    width: 2,
    thresholdMap: scaleMatrix([[0, 2], [3, 1]], 1 / 4)
  },
  [BAYER_3X3]: {
    width: 3,
    thresholdMap: scaleMatrix([[0, 7, 3], [6, 5, 2], [4, 1, 8]], 1 / 9)
  },
  [BAYER_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]],
      1 / 16
    )
  },
  [BAYER_8X8]: {
    width: 8,
    thresholdMap: scaleMatrix(
      [
        [0, 48, 12, 60, 3, 51, 15, 63],
        [32, 16, 44, 28, 35, 19, 47, 31],
        [8, 56, 4, 52, 11, 59, 7, 55],
        [40, 24, 36, 20, 43, 27, 39, 23],
        [2, 50, 14, 62, 1, 49, 13, 61],
        [34, 18, 46, 30, 33, 17, 45, 29],
        [10, 58, 6, 54, 9, 57, 5, 53],
        [42, 26, 38, 22, 41, 25, 37, 21]
      ],
      1 / 64
    )
  },
  [BAYER_16X16]: {
    width: 16,
    thresholdMap: scaleMatrix(
      // prettier-ignore
      [
        [   0,192, 48,240, 12,204, 60,252,  3,195, 51,243, 15,207, 63,255 ],
        [ 128, 64,176,112,140, 76,188,124,131, 67,179,115,143, 79,191,127 ],
        [  32,224, 16,208, 44,236, 28,220, 35,227, 19,211, 47,239, 31,223 ],
        [ 160, 96,144, 80,172,108,156, 92,163, 99,147, 83,175,111,159, 95 ],
        [   8,200, 56,248,  4,196, 52,244, 11,203, 59,251,  7,199, 55,247 ],
        [ 136, 72,184,120,132, 68,180,116,139, 75,187,123,135, 71,183,119 ],
        [  40,232, 24,216, 36,228, 20,212, 43,235, 27,219, 39,231, 23,215 ],
        [ 168,104,152, 88,164,100,148, 84,171,107,155, 91,167,103,151, 87 ],
        [   2,194, 50,242, 14,206, 62,254,  1,193, 49,241, 13,205, 61,253 ],
        [ 130, 66,178,114,142, 78,190,126,129, 65,177,113,141, 77,189,125 ],
        [  34,226, 18,210, 46,238, 30,222, 33,225, 17,209, 45,237, 29,221 ],
        [ 162, 98,146, 82,174,110,158, 94,161, 97,145, 81,173,109,157, 93 ],
        [  10,202, 58,250,  6,198, 54,246,  9,201, 57,249,  5,197, 53,245 ],
        [ 138, 74,186,122,134, 70,182,118,137, 73,185,121,133, 69,181,117 ],
        [  42,234, 26,218, 38,230, 22,214, 41,233, 25,217, 37,229, 21,213 ],
        [ 170,106,154, 90,166,102,150, 86,169,105,153, 89,165,101,149, 85]
      ],
      1 / 256
    )
  },
  [SQUARE_5X5]: {
    width: 5,
    thresholdMap: scaleMatrix(
      [
        [40, 60, 150, 90, 10],
        [80, 170, 240, 200, 110],
        [140, 210, 250, 220, 130],
        [120, 190, 230, 180, 70],
        [20, 100, 160, 50, 30]
      ],
      1 / 255
    )
  },
  [DISPERSED_DOT_3X3]: {
    width: 3,
    thresholdMap: scaleMatrix([[0, 6, 3], [4, 7, 2], [5, 1, 8]], 1 / 9)
  },
  [CORNER_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 2, 5, 9], [1, 4, 8, 12], [3, 7, 11, 14], [6, 10, 13, 15]],
      1 / 16
    )
  },
  [BLOCK_VERTICAL_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]],
      1 / 4
    ),
    levels: 4
  },
  [BLOCK_HORIZONTAL_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 0, 0, 0], [1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]],
      1 / 4
    ),
    levels: 4
  },
  [HATCH_2X2]: {
    width: 2,
    thresholdMap: scaleMatrix([[0, 1], [1, 0]], 1 / 2),
    levels: 2
  },
  [HATCH_3X3]: {
    width: 3,
    thresholdMap: scaleMatrix([[0, 1, 2], [1, 2, 1], [2, 1, 0]], 1 / 3),
    levels: 3
  },
  [HATCH_4X4]: {
    width: 4,
    thresholdMap: scaleMatrix(
      [[0, 1, 2, 3], [1, 2, 3, 2], [2, 3, 2, 1], [3, 2, 1, 0]],
      1 / 4
    ),
    levels: 4
  },
  [ALTERNATE_3X3]: {
    width: 3,
    thresholdMap: scaleMatrix([[0, 5, 1], [6, 2, 7], [3, 8, 4]], 1 / 9),
    levels: 9
  }
};

const getOrderedColor = (
  color: ColorRGBA,
  levels: number,
  tx: number,
  ty: number,
  threshold: Array<Array<?number>>
): ColorRGBA => {
  const thresholdValue = threshold[ty][tx];

  if (thresholdValue == null) {
    return rgba(255, 255, 0, 255); // error colour
  }

  const step = 255 / (levels - 1);

  // $FlowFixMe
  return color.map((c, i) => {
    if (i === 3) return c; // alpha channel
    const newColor = c + step * (thresholdValue - 0.5);
    const bucket = Math.round(newColor / step);
    return Math.round(bucket * step);
  });
};

export const optionTypes = {
  thresholdMap: {
    type: ENUM,
    options: [
      {
        name: "Bayer 2×2",
        value: BAYER_2X2
      },
      {
        name: "Bayer 3×3",
        value: BAYER_3X3
      },
      {
        name: "Bayer 4×4",
        value: BAYER_4X4
      },
      {
        name: "Bayer 8×8",
        value: BAYER_8X8
      },
      {
        name: "Bayer 16×16",
        value: BAYER_16X16
      },
      {
        name: "Dispersed Dot 3×3",
        value: DISPERSED_DOT_3X3
      },
      {
        name: "Digital Halftone 5×8",
        value: SQUARE_5X5
      },
      {
        name: "Corner 4×4",
        value: CORNER_4X4
      },
      {
        name: "Block Vertical 4×4",
        value: BLOCK_VERTICAL_4X4
      },
      {
        name: "Block Horizontal 4×4",
        value: BLOCK_HORIZONTAL_4X4
      },
      {
        name: "Hatch 2×2",
        value: HATCH_2X2
      },
      {
        name: "Hatch 3×3",
        value: HATCH_3X3
      },
      {
        name: "Hatch 4×4",
        value: HATCH_4X4
      },
      {
        name: "Alternate 3×3",
        value: ALTERNATE_3X3
      }
    ],
    default: BAYER_4X4
  },
  palette: { type: PALETTE, default: nearest }
};

const defaults = {
  thresholdMap: optionTypes.thresholdMap.default,
  palette: { ...optionTypes.palette.default, options: { levels: 2 } }
};

const ordered = (
  input: HTMLCanvasElement,
  options: {
    thresholdMap: Threshold,
    palette: Palette
  } = defaults
): HTMLCanvasElement => {
  const { palette, thresholdMap } = options;
  const levels =
    thresholdMap.levels || thresholdMap.length > 0
      ? thresholdMap.length * thresholdMap[0].length
      : 4;

  const output = cloneCanvas(input, false);

  const inputCtx = input.getContext("2d");
  const outputCtx = output.getContext("2d");

  if (!inputCtx || !outputCtx) {
    return input;
  }

  const buf = inputCtx.getImageData(0, 0, input.width, input.height).data;

  const threshold = thresholdMaps[thresholdMap];

  for (let x = 0; x < input.width; x += 1) {
    for (let y = 0; y < input.height; y += 1) {
      const tix = x % threshold.width;
      const tiy = y % threshold.width;
      const i = getBufferIndex(x, y, input.width);

      // Ignore alpha channel when calculating error
      const pixel = rgba(buf[i], buf[i + 1], buf[i + 2], buf[i + 3]);
      const orderedColor = getOrderedColor(
        pixel,
        levels,
        tix,
        tiy,
        threshold.thresholdMap
      );
      const color = palette.getColor(orderedColor, palette.options);

      fillBufferPixel(buf, i, color[0], color[1], color[2], buf[i + 3]);
    }
  }

  outputCtx.putImageData(new ImageData(buf, output.width, output.height), 0, 0);
  return output;
};

export default {
  name: "Ordered",
  func: ordered,
  options: defaults,
  optionTypes,
  defaults
};
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.audio',
  name: 'Beep',

  imports: [
    'setTimeout',
    'window'
  ],

  properties: [
    [ 'gain', 1.0 ],
    {
      class: 'Int',
      name: 'duration',
      value: 200,
      units: 'ms'
    },
    {
      name: 'type',
      value: 'triangle',
      view: { class: 'foam.u2.view.ChoiceView', choices: [ 'sine', 'square', 'sawtooth', 'triangle' ] },
    },
    { class: 'Float', name: 'frequency' , value: 220, units: 'Hz' },
    { class: 'Float', name: 'fmFrequency', label: 'FM Frequency', value: 0, units: 'Hz' },
    { class: 'Float', name: 'fmAmplitude', label: 'FM Amplitude', value: 20, units: '%' },
    {
      name: 'fmType',
      label: 'FM Type',
      value: 'triangle',
      view: { class: 'foam.u2.view.ChoiceView', choices: [ 'sine', 'square', 'sawtooth', 'triangle' ] },
    },
    { class: 'Float', name: 'amFrequency', label: 'AM Frequency', value: 0, units: 'Hz' },
    { class: 'Float', name: 'amAmplitude', label: 'AM Amplitude', value: 20, units: '%' },
    {
      name: 'amType',
      label: 'AM Type',
      value: 'triangle',
      view: { class: 'foam.u2.view.ChoiceView', choices: [ 'sine', 'square', 'sawtooth', 'triangle' ] },
    },
    {
      class: 'Boolean',
      name: 'envelope'
    },
    {
      class: 'Int',
      name: 'attack',
      units: 'ms'
    },
    {
      class: 'Int',
      name: 'decay',
      units: 'ms'
    },
    {
      class: 'Float',
      name: 'sustain',
      units: '%'
    },
    {
      class: 'Int',
      name: 'release',
      units: 'ms'
    }
  ],

  actions: [
    function play() {
      var audio       = new this.window.AudioContext();
      var destination = audio.destination;
      var o           = audio.createOscillator();
      var gain, fm, fmGain, am, amGain;

      if ( this.gain !== 1 || this.amFrequency ) {
        gain = audio.createGain();
        gain.gain.value = this.gain;
        gain.connect(destination);
        destination = gain;
      }

      o.frequency.value = this.frequency;
      o.type = this.type;
      o.connect(destination);

      if ( this.fmFrequency ) {
        fmGain = audio.createGain();
        fmGain.gain.value = this.fmAmplitude;
        fm = audio.createOscillator();
        fm.frequency.value = this.fmFrequency;
        fm.type = this.fmType;
        fm.connect(fmGain);
        fmGain.connect(o.frequency);
        fm.start();
      }

      if ( this.amFrequency ) {
        amGain = audio.createGain();
        amGain.gain.value = this.amAmplitude / 100;
        am = audio.createOscillator();
        am.frequency.value = this.amFrequency;
        am.type = this.amType;
        am.connect(amGain);
        amGain.connect(gain.gain);
        am.start();
      }

      o.start(0);

      o.stop(audio.currentTime + this.duration/1000);

      // There should be a better way to know when to cleanup.
      this.setTimeout(function() {
        if ( gain   ) gain.disconnect(audio.destination);
        if ( fmGain ) fmGain.disconnect(o.frequency);
        if ( fm     ) fm.stop(0);
        if ( amGain ) amGain.disconnect(gain.gain);
        if ( am     ) am.stop(0);
        o.disconnect(destination);
        audio.close();
      }, this.duration+1000);
    }
  ]
});
<commit_msg>Add attack/decay/sustain/release to Beep.<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.audio',
  name: 'Beep',

  imports: [
    'setTimeout',
    'window'
  ],

  properties: [
    [ 'gain', 1.0 ],
    {
      class: 'Int',
      name: 'duration',
      value: 200,
      units: 'ms'
    },
    {
      name: 'type',
      value: 'triangle',
      view: { class: 'foam.u2.view.ChoiceView', choices: [ 'sine', 'square', 'sawtooth', 'triangle' ] },
    },
    { class: 'Float', name: 'frequency' , value: 220, units: 'Hz' },
    { class: 'Float', name: 'fmFrequency', label: 'FM Frequency', value: 0, units: 'Hz' },
    { class: 'Float', name: 'fmAmplitude', label: 'FM Amplitude', value: 20, units: '%' },
    {
      name: 'fmType',
      label: 'FM Type',
      value: 'triangle',
      view: { class: 'foam.u2.view.ChoiceView', choices: [ 'sine', 'square', 'sawtooth', 'triangle' ] },
    },
    { class: 'Float', name: 'amFrequency', label: 'AM Frequency', value: 0, units: 'Hz' },
    { class: 'Float', name: 'amAmplitude', label: 'AM Amplitude', value: 20, units: '%' },
    {
      name: 'amType',
      label: 'AM Type',
      value: 'triangle',
      view: { class: 'foam.u2.view.ChoiceView', choices: [ 'sine', 'square', 'sawtooth', 'triangle' ] },
    },
    {
      class: 'Boolean',
      name: 'envelope'
    },
    {
      class: 'Int',
      name: 'attack',
      value: 100,
      units: 'ms'
    },
    {
      class: 'Int',
      name: 'decay',
      value: 100,
      units: 'ms'
    },
    {
      class: 'Float',
      name: 'sustain',
      value: 50,
      units: '%'
    },
    {
      class: 'Int',
      name: 'release',
      value: 100,
      units: 'ms'
    }
  ],

  actions: [
    function play() {
      var audio       = new this.window.AudioContext();
      var now         = audio.currentTime;
      var destination = audio.destination;
      var o           = audio.createOscillator();
      var gain, fm, fmGain, am, amGain, env;

      if ( this.gain !== 1 || this.amFrequency ) {
        gain = audio.createGain();
        gain.gain.value = this.gain;
        gain.connect(destination);
        destination = gain;
      }

      if ( this.envelope ) {
        env = audio.createGain();
        env.gain.cancelScheduledValues(0);
        env.gain.setValueAtTime(0, now);
        env.gain.linearRampToValueAtTime(1, now+this.attack/1000);
        env.gain.linearRampToValueAtTime(this.sustain/100, now+(this.attack+this.decay)/1000);
        env.gain.linearRampToValueAtTime(0, now+this.duration/1000);
        env.connect(destination);
        destination = env;
      }

      o.frequency.value = this.frequency;
      o.type = this.type;
      o.connect(destination);

      if ( this.fmFrequency ) {
        fmGain = audio.createGain();
        fmGain.gain.value = this.fmAmplitude;
        fm = audio.createOscillator();
        fm.frequency.value = this.fmFrequency;
        fm.type = this.fmType;
        fm.connect(fmGain);
        fmGain.connect(o.frequency);
        fm.start();
      }

      if ( this.amFrequency ) {
        amGain = audio.createGain();
        amGain.gain.value = this.amAmplitude / 100;
        am = audio.createOscillator();
        am.frequency.value = this.amFrequency;
        am.type = this.amType;
        am.connect(amGain);
        amGain.connect(gain.gain);
        am.start();
      }

      o.start(0);

      o.stop(now + this.duration/1000);

      // There should be a better way to know when to cleanup.
      this.setTimeout(function() {
        if ( gain   ) gain.disconnect(audio.destination);
        if ( env    ) env.disconnect(audio.destination);
        if ( fmGain ) fmGain.disconnect(o.frequency);
        if ( fm     ) fm.stop(0);
        if ( amGain ) amGain.disconnect(gain.gain);
        if ( am     ) am.stop(0);
        o.disconnect(destination);
        audio.close();
      }, this.duration+1000);
    }
  ]
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
Async functions compose other asynchronous functions that return promises.

One key to using these functions is to note that they return a function
that does the real work, so calling foam.async.sequence(myFuncArray),
for instance, doesn't create a promise or call any of the functions passed
to it. It instead sets up and returns a function that will return a promise
and evaluate arguments as needed.

To use the returned function, pass it to a Promise.then call:

Promise.resolve().then(foam.async.sequence( [ fn1, fn2 ] ));

Or create a new promise:

var p = new Promise(foam.async.sequence( [ fn1, fn2 ] ));

Async functions can also be nested:

var seq = foam.async.sequence([
  foam.async.log("Starting..."),
  foam.async.repeat(10, foam.async.sequence([
    function(i) { console.log("iteration", i); }),
    foam.async.sleep(1000)
  ])
]);
Promise.resolve().then(seq).then(foam.async.log("Done!"));

 */
foam.LIB({
  name: 'foam.async',

  methods: [
    function sequence(s) {
      /** Takes an array of functions (that may return a promise) and runs
        them one after anther. Functions that return a promise will have that
        promise chained, such that the next function will not run until the
        previous function's returned promise is resolved.

Errors are not handled, so chain any desired error handlers
        onto the promise returned.

You can use sequence's returned function directly in a then call:

promise.then(foam.async.sequence(...));

Or call it directly:

(foam.async.sequence(...))().then(...);

        @param {Array} s An array of functions that return Promises
        @returns {Function}  A function that returns a promise that will
                       resolve after the last function's return is resolved.
      */
      return function() {
        if ( ! s.length ) return Promise.resolve();

        var p = Promise.resolve();
        for ( var i = 0; i < s.length; ++i ) {
          p = p.then(s[i]);
        }
        return p;
      }
    },

    function awhile(condition, afunc) {
      /** Takes a condition function and runs the afunc while the condition
        returns true.
      */
      return function() {
        return new Promise(function(resolve, reject) {
          var awhile = function() {
            if (!condition()) {
              resolve();
              return;
            }
            return afunc().then(awhile);
          };
          awhile();
        });
      };
    },

    function repeat(times, fn) {
      /** Takes a function (that may return a promise) and runs it multiple
        times. A function that returns a promise will have that
        promise chained, such that the next call will not run until the
        previous call's returned promise is resolved. The function passed in
        will be called with one argument, the number of the iteration, from
        0 to times - 1.

Errors are not handled, so chain any desired error handlers
        onto the promise returned.

You can use repeat's returned function directly in a then call:

promise.then(foam.async.repeat(...));

Or call it directly:

(foam.async.repeat(...))().then(...);

        @param {Number} times number of times to repeat in sequence.
        @param {Function} fn Function that returns a Promise.
        @returns {Function}  A function that returns a Promise that will resolve
                       after the last repetition's return resolves.
      */
      return function() {
        var p = Promise.resolve();
        var n = 0;
        for ( var i = 0; i < times; ++i ) {
          p = p.then(function() { return fn(n++); });
        }
        return p;
      };
    },

    /**
      Takes a function (that may return a promise) and runs it multiple
      times in parallel. A function that returns a promise will have that
      promise chained, such that the entire group will not resolve until
      all returned promises have resolved (as in the standard Promise.all);
      The function passed in
      will be called with one argument, the number of the iteration, from
      0 to times - 1.

Errors are not handled, so chain any desired error handlers
      onto the promise returned.

You can use repeatParallel's returned function directly in a then call:

promise.then(foam.async.repeatParallel(...));

Or call it directly:

(foam.async.repeatParallel(...))().then(...);

      @param {Number} times number of times to repeat in sequence.
      @param {Function} fn Function that returns a Promise.
      @returns {Function}  A function that returns a Promise that will resolve
                   after every repetition's return resolves
    */
    function repeatParallel(times, fn) {
      return function() {
        var promises = [];
        for ( var i = 0; i < times; ++i ) {
          promises[i] = fn(i); // TODO: what if not returned a promise?
        }
        return Promise.all(promises);
      };
    },

    function log() {
      /** Returns a function you can pass to a .then call, or other foam.async
        functions. Takes variable arguments that are passed to console.log. */
      var args = arguments;
      return function() {
        console.log.apply(console, args);
        return Promise.resolve();
      };
    },

    function sleep(/* Number */ time) {
      /** Returns a function that returns a promise that delays by the given
        time before resolving. */
      return function() {
        return new Promise(function(resolve, reject) {
          setTimeout(function() { resolve(); }, time);
        });
      };
    }
  ]
});
<commit_msg>Remove awhile because it doesn't belong in this branch and doesn't have anything using it yet.<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
Async functions compose other asynchronous functions that return promises.

One key to using these functions is to note that they return a function
that does the real work, so calling foam.async.sequence(myFuncArray),
for instance, doesn't create a promise or call any of the functions passed
to it. It instead sets up and returns a function that will return a promise
and evaluate arguments as needed.

To use the returned function, pass it to a Promise.then call:

Promise.resolve().then(foam.async.sequence( [ fn1, fn2 ] ));

Or create a new promise:

var p = new Promise(foam.async.sequence( [ fn1, fn2 ] ));

Async functions can also be nested:

var seq = foam.async.sequence([
  foam.async.log("Starting..."),
  foam.async.repeat(10, foam.async.sequence([
    function(i) { console.log("iteration", i); }),
    foam.async.sleep(1000)
  ])
]);
Promise.resolve().then(seq).then(foam.async.log("Done!"));

 */
foam.LIB({
  name: 'foam.async',

  methods: [
    function sequence(s) {
      /** Takes an array of functions (that may return a promise) and runs
        them one after anther. Functions that return a promise will have that
        promise chained, such that the next function will not run until the
        previous function's returned promise is resolved.

Errors are not handled, so chain any desired error handlers
        onto the promise returned.

You can use sequence's returned function directly in a then call:

promise.then(foam.async.sequence(...));

Or call it directly:

(foam.async.sequence(...))().then(...);

        @param {Array} s An array of functions that return Promises
        @returns {Function}  A function that returns a promise that will
                       resolve after the last function's return is resolved.
      */
      return function() {
        if ( ! s.length ) return Promise.resolve();

        var p = Promise.resolve();
        for ( var i = 0; i < s.length; ++i ) {
          p = p.then(s[i]);
        }
        return p;
      }
    },

    function repeat(times, fn) {
      /** Takes a function (that may return a promise) and runs it multiple
        times. A function that returns a promise will have that
        promise chained, such that the next call will not run until the
        previous call's returned promise is resolved. The function passed in
        will be called with one argument, the number of the iteration, from
        0 to times - 1.

Errors are not handled, so chain any desired error handlers
        onto the promise returned.

You can use repeat's returned function directly in a then call:

promise.then(foam.async.repeat(...));

Or call it directly:

(foam.async.repeat(...))().then(...);

        @param {Number} times number of times to repeat in sequence.
        @param {Function} fn Function that returns a Promise.
        @returns {Function}  A function that returns a Promise that will resolve
                       after the last repetition's return resolves.
      */
      return function() {
        var p = Promise.resolve();
        var n = 0;
        for ( var i = 0; i < times; ++i ) {
          p = p.then(function() { return fn(n++); });
        }
        return p;
      };
    },

    /**
      Takes a function (that may return a promise) and runs it multiple
      times in parallel. A function that returns a promise will have that
      promise chained, such that the entire group will not resolve until
      all returned promises have resolved (as in the standard Promise.all);
      The function passed in
      will be called with one argument, the number of the iteration, from
      0 to times - 1.

Errors are not handled, so chain any desired error handlers
      onto the promise returned.

You can use repeatParallel's returned function directly in a then call:

promise.then(foam.async.repeatParallel(...));

Or call it directly:

(foam.async.repeatParallel(...))().then(...);

      @param {Number} times number of times to repeat in sequence.
      @param {Function} fn Function that returns a Promise.
      @returns {Function}  A function that returns a Promise that will resolve
                   after every repetition's return resolves
    */
    function repeatParallel(times, fn) {
      return function() {
        var promises = [];
        for ( var i = 0; i < times; ++i ) {
          promises[i] = fn(i); // TODO: what if not returned a promise?
        }
        return Promise.all(promises);
      };
    },

    function log() {
      /** Returns a function you can pass to a .then call, or other foam.async
        functions. Takes variable arguments that are passed to console.log. */
      var args = arguments;
      return function() {
        console.log.apply(console, args);
        return Promise.resolve();
      };
    },

    function sleep(/* Number */ time) {
      /** Returns a function that returns a promise that delays by the given
        time before resolving. */
      return function() {
        return new Promise(function(resolve, reject) {
          setTimeout(function() { resolve(); }, time);
        });
      };
    }
  ]
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.core',
  name: 'Int',
  extends: 'Property',

  properties: [
    'units',
    [ 'value', 0 ],
    'min',
    'max',
    [ 'adapt', function adaptInt(_, v) {
        return typeof v === 'number' ? Math.trunc(v) :
          v ? parseInt(v) :
          0 ;
      }
    ],
    [ 'fromString', function intFromString(str) {
        return str ? parseInt(str) : 0;
      }
    ]
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'String',
  extends: 'Property',

  documentation: 'StringProperties coerce their arguments into Strings.',

  properties: [
    { class: 'Int', name: 'width', value: 30 },
    [ 'adapt', function(_, a) {
        return typeof a === 'function' ? foam.String.multiline(a) :
               typeof a === 'number'   ? String(a)                :
               a && a.toString         ? a.toString()             :
                                         ''                       ;
      }
    ],
    [ 'value', '' ]
  ]
});

foam.CLASS({
  refines: 'foam.core.Model',

  documentation: 'Upgrade Mode.documentation to a proper String property.',

  properties: [
    { class: 'String', name: 'documentation' }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'Date',
  extends: 'Property',

  // documentation: 'Describes properties of type Date.',
  label: 'Date',

  properties: [
    {
      name: 'adapt',
      value: function (_, d) {
        if ( typeof d === 'number' ) return new Date(d);
        if ( typeof d === 'string' ) {
          var ret = new Date(d);

          if ( isNaN(ret.getTime()) ) throw 'Invalid Date: ' + d;

          return ret;
        }
        return d;
      }
    },
    {
      name: 'comparePropertyValues',
      value: function(o1, o2) {
        if ( ! o1 ) return o2 ? -1 : 0;
        if ( ! o2 ) return 1;

        return foam.Date.compare(o1, o2);
      }
    }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'DateTime',
  extends: 'Date',

  documentation: 'Describes properties of type DateTime.',
  label: 'Date and time'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Byte',
  extends: 'Int',

  documentation: 'Describes properties of type Byte.',
  label: 'Round byte numbers'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Short',
  extends: 'Int',

  documentation: 'Describes properties of type Short.',
  label: 'Round short numbers'
});

foam.CLASS({
  package: 'foam.core',
  name:  'Long',
  extends: 'Int',

  documentation:  'Describes properties of type Long.',
  label: 'Round long numbers'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Float',
  extends: 'Int',

  // documentation:  'Describes properties of type Float.',
  label: 'Decimal numbers',

  properties: [
    'precision',
    [
      'adapt',
      function (_, v) {
        return typeof v === 'number' ? v : v ? parseFloat(v) : 0.0 ;
      }
    ]
  ]
});

/**
 No different than Float for JS, but useful when targeting with other languages.
 **/
foam.CLASS({
  package: 'foam.core',
  name: 'Double',
  extends: 'Float'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Function',
  extends: 'Property',

  documentation: 'Describes properties of type Function.',
  label: 'Code that can be run',

  properties: [
    [
      'value',
      function() {}
    ],
    [
      'assertValue',
      function(value, prop) {
        foam.assert(typeof value === 'function', prop.name, 'Cannot set to non function type.');
      }
    ]
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'Blob',
  extends: 'Property',

  // documentation: 'A chunk of binary data.',
  label: 'Binary data',
});

foam.CLASS({
  package: 'foam.core',
  name: 'Object',
  extends: 'Property',
  documentation: ''
});

foam.CLASS({
  package: 'foam.core',
  name: 'Array',
  extends: 'Property',

  properties: [
    [
      'factory',
      function() { return []; }
    ],
    [
      'isDefaultValue',
      function(v) { return ! v || ! v.length; }
    ]
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'List',
  extends: 'foam.core.Object'
});

foam.CLASS({
  package: 'foam.core',
  name: 'StringArray',
  extends: 'Property',

  documentation: 'An array of String values.',
  label: 'List of text strings',

  properties: [
    {
      name: 'of',
      value: 'String',
      documentation: 'The FOAM sub-type of this property.'
    },
    [
      'factory',
      function() { return []; }
    ],
    [
      'adapt',
      function(_, v, prop) {
        if ( ! Array.isArray(v) ) return v;

        var copy;
        for ( var i = 0 ; i < v.length ; i++ ) {
          if ( typeof v[i] !== 'string' ) {
            if ( ! copy ) copy = v.slice();
            copy[i] = '' + v[i];
          }
        }

        return copy || v;
      }
    ],
    [
      'assertValue',
      function(v, prop) {
        if ( v === null ) return;

        foam.assert(Array.isArray(v),
            prop.name, 'Tried to set StringArray to non-array type.');
        for ( var i = 0 ; i < v.length ; i++ ) {
          foam.assert(typeof v[i] === 'string',
              prop.name, 'Element', i, 'is not a string', v[i]);
        }
      }
    ]
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'Class',
  extends: 'Property',

  properties: [
    {
      name: 'getter',
      value: function(prop) {
        var c = this.instance_[prop.name];

        // Implement value and factory support.
        if ( foam.Undefined.isInstance(c) ) {
          if ( ! foam.Undefined.isInstance(prop.value) ) {
            c = prop.value;
          } else if ( prop.factory ) {
            c = this.instance_[prop.name] = prop.factory.call(this, prop);
          }
        }

        // Upgrade Strings to actual classes, if available.
        if ( foam.String.isInstance(c) ) {
          c = this.lookup(c, true);
          if ( c ) this.instance_[prop.name] = c;
        }

        return c;
      }
    },
    {
      name: 'toJSON',
      value: function(value) { return value ? value.id : value; }
    }
  ],

  methods: [
    function installInProto(proto) {
      this.SUPER(proto);

      var name = this.name;

      Object.defineProperty(proto, name + '$cls', {
        get: function classGetter() {
          console.warn("Deprecated use of 'cls.$cls'. Just use 'cls' instead.");
          return typeof this[name] !== 'string' ? this[name] :
            this.__context__.lookup(this[name], true);
        },
        configurable: true
      });
    }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'EMail',
  extends: 'String',
  // FUTURE: verify
  label: 'Email address'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Image',
  extends: 'String',
  // FUTURE: verify
  label: 'Image data or link'
});

foam.CLASS({
  package: 'foam.core',
  name: 'URL',
  extends: 'String',
  // FUTURE: verify
  label: 'Web link (URL or internet address)'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Color',
  extends: 'String',
  label: 'Color'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Password',
  extends: 'String',
  label: 'Password that displays protected or hidden text'
});

foam.CLASS({
  package: 'foam.core',
  name: 'PhoneNumber',
  extends: 'String',
  label: 'Phone number'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Currency',
  extends: 'Long'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Map',
  extends: 'Property',

  // TODO: Remove need for sorting
  properties: [
    [ 'factory', function() { return {} } ],
    [
      'comparePropertyValues',
      function(o1, o2) {
        if ( foam.typeOf(o1) != foam.typeOf(o2) ) return -1;

        var keys1 = Object.keys(o1).sort();
        var keys2 = Object.keys(o2).sort();
        if ( keys1.length < keys2.length ) return -1;
        if ( keys1.length > keys2.length ) return 1;
        for ( var i = 0 ; i < keys1.length ; i++ ) {
          var c = foam.String.compare(keys1[i], keys2[i]);
          if ( c != 0 ) return c;
          c = foam.util.compare(o1[keys1[i]], o2[keys2[i]]);
          if ( c != 0 ) return c;
        }

        return 0;
      }
    ],
    [
      'diffPropertyValues',
      function(o1, o2) {
        // TODO
      }
    ],
    'of'
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'FObjectProperty',
  extends: 'Property',

  properties: [
    {
      class: 'Class',
      name: 'of'
    },
    {
      name: 'fromJSON',
      value: function(json, ctx, prop) {
        return foam.json.parse(json, prop.of, ctx);
      }
    },
    {
      name: 'adapt',
      value: function(_, v, prop) {
        // All FObjects may be null.
        if (v === null) return v;

        var of = prop.of;

        return of.isInstance(v) ?
            v :
            ( v.class ?
                this.lookup(v.class) :
                of ).create(v, this.__subContext__);
      }
    }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'Reference',
  extends: 'Property',

  properties: [
    {
      class: 'Class',
      name: 'of'
    },
    {
      class: 'String',
      name: 'targetDAOKey',
      expression: function(of) { return foam.String.daoize(of.name); }
    },
    {
      name: 'adapt',
      value: function(oldValue, newValue, prop) {
        return prop.of.isInstance(newValue) ?
          newValue.id :
          newValue ;
      }
    }
  ],

  methods: [
    function installInProto(proto) {
      this.SUPER(proto);
      var key  = this.targetDAOKey;
      var name = this.name;

      Object.defineProperty(proto, name + '$find', {
        get: function classGetter() {
          return this.__context__[key].find(this[name]);
        },
        configurable: true
      });
    }
  ]
});

foam.CLASS({
  refines: 'foam.core.Model',

  documentation: 'Update Model Property types.',

  properties: [
    { class: 'String',  name: 'name' },
    { class: 'Boolean', name: 'abstract' }
  ]
});

foam.CLASS({
  refines: 'Property',

  axioms: [
    foam.pattern.Faceted.create()
  ],

  properties: [
    {
      name: 'of'
    }
  ]
});

foam.CLASS({
  refines: 'Property',

  properties: [
    /**
      A short-name is an optional shorter name for a property.
      It is used by JSON and XML support when 'useShortNames'
      is enabled. Short-names enable output to be smaller,
      which can save disk space and/or network bandwidth.
      Ex.
    <pre>
      properties: [
        { name: 'firstName', shortName: 'fn' },
        { name: 'lastName',  shortName: 'ln' }
      ]
    </pre>
    */
    { class: 'String', name: 'shortName' }
  ]
});
<commit_msg>Revert previous change to FObjectProperty.of.value.<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.core',
  name: 'Int',
  extends: 'Property',

  properties: [
    'units',
    [ 'value', 0 ],
    'min',
    'max',
    [ 'adapt', function adaptInt(_, v) {
        return typeof v === 'number' ? Math.trunc(v) :
          v ? parseInt(v) :
          0 ;
      }
    ],
    [ 'fromString', function intFromString(str) {
        return str ? parseInt(str) : 0;
      }
    ]
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'String',
  extends: 'Property',

  documentation: 'StringProperties coerce their arguments into Strings.',

  properties: [
    { class: 'Int', name: 'width', value: 30 },
    [ 'adapt', function(_, a) {
        return typeof a === 'function' ? foam.String.multiline(a) :
               typeof a === 'number'   ? String(a)                :
               a && a.toString         ? a.toString()             :
                                         ''                       ;
      }
    ],
    [ 'value', '' ]
  ]
});

foam.CLASS({
  refines: 'foam.core.Model',

  documentation: 'Upgrade Mode.documentation to a proper String property.',

  properties: [
    { class: 'String', name: 'documentation' }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'Date',
  extends: 'Property',

  // documentation: 'Describes properties of type Date.',
  label: 'Date',

  properties: [
    {
      name: 'adapt',
      value: function (_, d) {
        if ( typeof d === 'number' ) return new Date(d);
        if ( typeof d === 'string' ) {
          var ret = new Date(d);

          if ( isNaN(ret.getTime()) ) throw 'Invalid Date: ' + d;

          return ret;
        }
        return d;
      }
    },
    {
      name: 'comparePropertyValues',
      value: function(o1, o2) {
        if ( ! o1 ) return o2 ? -1 : 0;
        if ( ! o2 ) return 1;

        return foam.Date.compare(o1, o2);
      }
    }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'DateTime',
  extends: 'Date',

  documentation: 'Describes properties of type DateTime.',
  label: 'Date and time'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Byte',
  extends: 'Int',

  documentation: 'Describes properties of type Byte.',
  label: 'Round byte numbers'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Short',
  extends: 'Int',

  documentation: 'Describes properties of type Short.',
  label: 'Round short numbers'
});

foam.CLASS({
  package: 'foam.core',
  name:  'Long',
  extends: 'Int',

  documentation:  'Describes properties of type Long.',
  label: 'Round long numbers'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Float',
  extends: 'Int',

  // documentation:  'Describes properties of type Float.',
  label: 'Decimal numbers',

  properties: [
    'precision',
    [
      'adapt',
      function (_, v) {
        return typeof v === 'number' ? v : v ? parseFloat(v) : 0.0 ;
      }
    ]
  ]
});

/**
 No different than Float for JS, but useful when targeting with other languages.
 **/
foam.CLASS({
  package: 'foam.core',
  name: 'Double',
  extends: 'Float'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Function',
  extends: 'Property',

  documentation: 'Describes properties of type Function.',
  label: 'Code that can be run',

  properties: [
    [
      'value',
      function() {}
    ],
    [
      'assertValue',
      function(value, prop) {
        foam.assert(typeof value === 'function', prop.name, 'Cannot set to non function type.');
      }
    ]
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'Blob',
  extends: 'Property',

  // documentation: 'A chunk of binary data.',
  label: 'Binary data',
});

foam.CLASS({
  package: 'foam.core',
  name: 'Object',
  extends: 'Property',
  documentation: ''
});

foam.CLASS({
  package: 'foam.core',
  name: 'Array',
  extends: 'Property',

  properties: [
    [
      'factory',
      function() { return []; }
    ],
    [
      'isDefaultValue',
      function(v) { return ! v || ! v.length; }
    ]
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'List',
  extends: 'foam.core.Object'
});

foam.CLASS({
  package: 'foam.core',
  name: 'StringArray',
  extends: 'Property',

  documentation: 'An array of String values.',
  label: 'List of text strings',

  properties: [
    {
      name: 'of',
      value: 'String',
      documentation: 'The FOAM sub-type of this property.'
    },
    [
      'factory',
      function() { return []; }
    ],
    [
      'adapt',
      function(_, v, prop) {
        if ( ! Array.isArray(v) ) return v;

        var copy;
        for ( var i = 0 ; i < v.length ; i++ ) {
          if ( typeof v[i] !== 'string' ) {
            if ( ! copy ) copy = v.slice();
            copy[i] = '' + v[i];
          }
        }

        return copy || v;
      }
    ],
    [
      'assertValue',
      function(v, prop) {
        if ( v === null ) return;

        foam.assert(Array.isArray(v),
            prop.name, 'Tried to set StringArray to non-array type.');
        for ( var i = 0 ; i < v.length ; i++ ) {
          foam.assert(typeof v[i] === 'string',
              prop.name, 'Element', i, 'is not a string', v[i]);
        }
      }
    ]
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'Class',
  extends: 'Property',

  properties: [
    {
      name: 'getter',
      value: function(prop) {
        var c = this.instance_[prop.name];

        // Implement value and factory support.
        if ( foam.Undefined.isInstance(c) ) {
          if ( ! foam.Undefined.isInstance(prop.value) ) {
            c = prop.value;
          } else if ( prop.factory ) {
            c = this.instance_[prop.name] = prop.factory.call(this, prop);
          }
        }

        // Upgrade Strings to actual classes, if available.
        if ( foam.String.isInstance(c) ) {
          c = this.lookup(c, true);
          if ( c ) this.instance_[prop.name] = c;
        }

        return c;
      }
    },
    {
      name: 'toJSON',
      value: function(value) { return value ? value.id : value; }
    }
  ],

  methods: [
    function installInProto(proto) {
      this.SUPER(proto);

      var name = this.name;

      Object.defineProperty(proto, name + '$cls', {
        get: function classGetter() {
          console.warn("Deprecated use of 'cls.$cls'. Just use 'cls' instead.");
          return typeof this[name] !== 'string' ? this[name] :
            this.__context__.lookup(this[name], true);
        },
        configurable: true
      });
    }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'EMail',
  extends: 'String',
  // FUTURE: verify
  label: 'Email address'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Image',
  extends: 'String',
  // FUTURE: verify
  label: 'Image data or link'
});

foam.CLASS({
  package: 'foam.core',
  name: 'URL',
  extends: 'String',
  // FUTURE: verify
  label: 'Web link (URL or internet address)'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Color',
  extends: 'String',
  label: 'Color'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Password',
  extends: 'String',
  label: 'Password that displays protected or hidden text'
});

foam.CLASS({
  package: 'foam.core',
  name: 'PhoneNumber',
  extends: 'String',
  label: 'Phone number'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Currency',
  extends: 'Long'
});

foam.CLASS({
  package: 'foam.core',
  name: 'Map',
  extends: 'Property',

  // TODO: Remove need for sorting
  properties: [
    [ 'factory', function() { return {} } ],
    [
      'comparePropertyValues',
      function(o1, o2) {
        if ( foam.typeOf(o1) != foam.typeOf(o2) ) return -1;

        var keys1 = Object.keys(o1).sort();
        var keys2 = Object.keys(o2).sort();
        if ( keys1.length < keys2.length ) return -1;
        if ( keys1.length > keys2.length ) return 1;
        for ( var i = 0 ; i < keys1.length ; i++ ) {
          var c = foam.String.compare(keys1[i], keys2[i]);
          if ( c != 0 ) return c;
          c = foam.util.compare(o1[keys1[i]], o2[keys2[i]]);
          if ( c != 0 ) return c;
        }

        return 0;
      }
    ],
    [
      'diffPropertyValues',
      function(o1, o2) {
        // TODO
      }
    ],
    'of'
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'FObjectProperty',
  extends: 'Property',

  properties: [
    {
      class: 'Class',
      name: 'of',
      value: 'foam.core.FObject'
    },
    {
      name: 'fromJSON',
      value: function(json, ctx, prop) {
        return foam.json.parse(json, prop.of, ctx);
      }
    },
    {
      name: 'adapt',
      value: function(_, v, prop) {
        // All FObjects may be null.
        if (v === null) return v;

        var of = prop.of;

        return of.isInstance(v) ?
            v :
            ( v.class ?
                this.lookup(v.class) :
                of ).create(v, this.__subContext__);
      }
    }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'Reference',
  extends: 'Property',

  properties: [
    {
      class: 'Class',
      name: 'of'
    },
    {
      class: 'String',
      name: 'targetDAOKey',
      expression: function(of) { return foam.String.daoize(of.name); }
    },
    {
      name: 'adapt',
      value: function(oldValue, newValue, prop) {
        return prop.of.isInstance(newValue) ?
          newValue.id :
          newValue ;
      }
    }
  ],

  methods: [
    function installInProto(proto) {
      this.SUPER(proto);
      var key  = this.targetDAOKey;
      var name = this.name;

      Object.defineProperty(proto, name + '$find', {
        get: function classGetter() {
          return this.__context__[key].find(this[name]);
        },
        configurable: true
      });
    }
  ]
});

foam.CLASS({
  refines: 'foam.core.Model',

  documentation: 'Update Model Property types.',

  properties: [
    { class: 'String',  name: 'name' },
    { class: 'Boolean', name: 'abstract' }
  ]
});

foam.CLASS({
  refines: 'Property',

  axioms: [
    foam.pattern.Faceted.create()
  ],

  properties: [
    {
      name: 'of'
    }
  ]
});

foam.CLASS({
  refines: 'Property',

  properties: [
    /**
      A short-name is an optional shorter name for a property.
      It is used by JSON and XML support when 'useShortNames'
      is enabled. Short-names enable output to be smaller,
      which can save disk space and/or network bandwidth.
      Ex.
    <pre>
      properties: [
        { name: 'firstName', shortName: 'fn' },
        { name: 'lastName',  shortName: 'ln' }
      ]
    </pre>
    */
    { class: 'String', name: 'shortName' }
  ]
});
<|endoftext|>


<commit_before>import {BlockQuote, OrderedList, BulletList, ListItem,
        HorizontalRule, Paragraph, Heading, CodeBlock, Image, HardBreak,
        EmMark, StrongMark, LinkMark, CodeMark, Node} from "../model"
import sortedInsert from "../util/sortedinsert"
import {defineSource} from "./register"

// :: (Schema, DOMNode, ?Object) → Node
// Parse document from the content of a DOM node. To pass an explicit
// parent document (for example, when not in a browser window
// environment, where we simply use the global document), pass it as
// the `document` property of `options`.
export function fromDOM(schema, dom, options) {
  if (!options) options = {}
  let context = new DOMParseState(schema, options.topNode || schema.node("doc"), options)
  let start = options.from ? dom.childNodes[options.from] : dom.firstChild
  let end = options.to != null && dom.childNodes[options.to] || null
  context.addAll(start, end, true)
  let doc
  while (context.stack.length) doc = context.leave()
  return doc
}

// ;; #path=DOMParseSpec #kind=interface
// To define the way [node](#NodeType) and [mark](#MarkType) types are
// parsed, you can associate one or more DOM parsing specifications to
// them using the [`register`](#SchemaItem.register) method with the
// `"parseDOM"` namespace, using the HTML node name (lowercase) as
// value name. Each of them defines a parsing strategy for a certain
// type of DOM node. When `"_"` is used as name, the parser is
// activated for all nodes.

// :: ?number #path=DOMParseSpec.rank
// The precedence of this parsing strategy. Should be a number between
// 0 and 100, which determines when this parser gets a chance relative
// to others that apply to the node (low ranks go first). Defaults to
// 50.

// :: union<string, (dom: DOMNode, state: DOMParseState) → ?bool> #path=DOMParseSpec.parse
// The function that, given a DOM node, parses it, updating the parse
// state. It should return (the exact value) `false` when it wants to
// indicate that it was not able to parse this node. This function is
// called in such a way that `this` is bound to the type that the
// parse spec was associated with.
//
// When this is set to the string `"block"`, the content of the DOM
// node is parsed as the content in a node of the type that this spec
// was associated with.
//
// When set to the string `"mark"`, the content of the DOM node is
// parsed with an instance of the mark that this spec was associated
// with added to their marks.

defineSource("dom", fromDOM)

// :: (Schema, string, ?Object) → Node
// Parses the HTML into a DOM, and then calls through to `fromDOM`.
export function fromHTML(schema, html, options) {
  let wrap = (options && options.document || window.document).createElement("div")
  wrap.innerHTML = html
  return fromDOM(schema, wrap, options)
}

defineSource("html", fromHTML)

const blockElements = {
  address: true, article: true, aside: true, blockquote: true, canvas: true,
  dd: true, div: true, dl: true, fieldset: true, figcaption: true, figure: true,
  footer: true, form: true, h1: true, h2: true, h3: true, h4: true, h5: true,
  h6: true, header: true, hgroup: true, hr: true, li: true, noscript: true, ol: true,
  output: true, p: true, pre: true, section: true, table: true, tfoot: true, ul: true
}

const ignoreElements = {
  head: true, noscript: true, object: true, script: true, style: true, title: true
}

const noMarks = []

// ;; A state object used to track context during a parse,
// and to expose methods to custom parsing functions.
class DOMParseState {
  constructor(schema, topNode, options) {
    // :: Object The options passed to this parse.
    this.options = options || {}
    // :: Schema The schema that we are parsing into.
    this.schema = schema
    this.stack = []
    this.marks = noMarks
    this.closing = false
    this.enter(topNode.type, topNode.attrs)
    let info = schemaInfo(schema)
    this.tagInfo = info.tags
    this.styleInfo = info.styles
  }

  get top() {
    return this.stack[this.stack.length - 1]
  }

  addDOM(dom) {
    if (dom.nodeType == 3) {
      let value = dom.nodeValue
      let top = this.top, last
      if (/\S/.test(value) || top.type.isTextblock) {
        if (!this.options.preserveWhitespace) {
          value = value.replace(/\s+/g, " ")
          // If this starts with whitespace, and there is either no node
          // before it or a node that ends with whitespace, strip the
          // leading space.
          if (/^\s/.test(value) &&
              (!(last = top.content[top.content.length - 1]) ||
               (last.type.name == "text" && /\s$/.test(last.text))))
            value = value.slice(1)
        }
        if (value)
          this.insertNode(this.schema.text(value, this.marks))
      }
    } else if (dom.nodeType != 1 || dom.hasAttribute("pm-ignore")) {
      // Ignore non-text non-element nodes
    } else {
      let style = dom.getAttribute("style")
      if (style) this.addElementWithStyles(parseStyles(style), dom)
      else this.addElement(dom)
    }
  }

  addElement(dom) {
    let name = dom.nodeName.toLowerCase()
    if (!this.parseNodeType(name, dom) && !ignoreElements.hasOwnProperty(name)) {
      this.addAll(dom.firstChild, null)
      if (blockElements.hasOwnProperty(name) && this.top.type == this.schema.defaultTextblockType())
        this.closing = true
    }
  }

  addElementWithStyles(styles, dom) {
    let wrappers = []
    for (let i = 0; i < styles.length; i += 2) {
      let parsers = this.styleInfo[styles[i]], value = styles[i + 1]
      if (parsers) for (let j = 0; j < parsers.length; j++)
        wrappers.push(parsers[j], value)
    }
    let next = (i) => {
      if (i == wrappers.length) {
        this.addElement(dom)
      } else {
        let parser = wrappers[i]
        parser.parse.call(parser.type, wrappers[i + 1], this, next.bind(null, i + 2))
      }
    }
    next(0)
  }

  tryParsers(parsers, dom) {
    if (parsers) for (let i = 0; i < parsers.length; i++) {
      let parser = parsers[i]
      if (parser.parse.call(parser.type, dom, this) !== false) return true
    }
  }

  parseNodeType(name, dom) {
    return this.tryParsers(this.tagInfo[name], dom) ||
      this.tryParsers(this.tagInfo._, dom)
  }

  addAll(from, to, sync) {
    let stack = sync && this.stack.slice()
    for (let dom = from; dom != to; dom = dom.nextSibling) {
      this.addDOM(dom)
      if (sync && blockElements.hasOwnProperty(dom.nodeName.toLowerCase()))
        this.sync(stack)
    }
  }

  doClose() {
    if (!this.closing || this.stack.length < 2) return
    let left = this.leave()
    this.enter(left.type, left.attrs)
    this.closing = false
  }

  insertNode(node) {
    if (this.top.type.canContain(node)) {
      this.doClose()
    } else {
      for (let i = this.stack.length - 1; i >= 0; i--) {
        let route = this.stack[i].type.findConnection(node.type)
        if (!route) continue
        if (i == this.stack.length - 1) {
          this.doClose()
        } else {
          while (this.stack.length > i + 1) this.leave()
        }
        for (let j = 0; j < route.length; j++)
          this.enter(route[j])
        if (this.marks.length) this.marks = noMarks
        break
      }
    }
    this.top.content.push(node)
    return node
  }

  // :: (DOMNode, NodeType, ?Object, [Node]) → Node
  // Insert a node of the given type, with the given content, based on
  // `dom`, at the current position in the document.
  insert(type, attrs, content) {
    return this.insertNode(type.createAutoFill(attrs, content, this.marks))
  }

  enter(type, attrs) {
    this.stack.push({type, attrs, content: []})
  }

  leave() {
    if (this.marks.length) this.marks = noMarks
    let top = this.stack.pop()
    let last = top.content[top.content.length - 1]
    if (!this.options.preserveWhitespace && last && last.isText && /\s$/.test(last.text))
      top.content[top.content.length - 1] = last.copy(last.text.slice(0, last.text.length - 1))
    let node = top.type.createAutoFill(top.attrs, top.content)
    if (this.stack.length) this.insertNode(node)
    return node
  }

  sync(stack) {
    while (this.stack.length > stack.length) this.leave()
    for (;;) {
      let n = this.stack.length - 1, one = this.stack[n], two = stack[n]
      if (one.type == two.type && Node.sameAttrs(one.attrs, two.attrs)) break
      this.leave()
    }
    while (stack.length > this.stack.length) {
      let add = stack[this.stack.length]
      this.enter(add.type, add.attrs)
    }
    if (this.marks.length) this.marks = noMarks
    this.closing = false
  }

  // :: (DOMNode, NodeType, ?Object)
  // Parse the contents of `dom` as children of a node of the given
  // type.
  wrapIn(dom, type, attrs) {
    this.enter(type, attrs)
    this.addAll(dom.firstChild, null, true)
    this.leave()
  }

  // :: (DOMNode, Mark)
  // Parse the contents of `dom`, with `mark` added to the set of
  // current marks.
  wrapMark(inner, mark) {
    let old = this.marks
    this.marks = (mark.instance || mark).addToSet(old)
    if (inner.call) inner()
    else this.addAll(inner.firstChild, null)
    this.marks = old
  }
}

function parseStyles(style) {
  let re = /\s*([\w-]+)\s*:\s*([^;]+)/g, m, result = []
  while (m = re.exec(style)) result.push(m[1], m[2].trim())
  return result
}

function schemaInfo(schema) {
  return schema.cached.parseDOMInfo || (schema.cached.parseDOMInfo = summarizeSchemaInfo(schema))
}

function summarizeSchemaInfo(schema) {
  let tags = Object.create(null), styles = Object.create(null)
  tags._ = []
  schema.registry("parseDOM", (tag, info, type) => {
    let parse = info.parse
    if (parse == "block")
      parse = function(dom, state) { state.wrapIn(dom, this) }
    else if (parse == "mark")
      parse = function(dom, state) { state.wrapMark(dom, this) }
    sortedInsert(tags[tag] || (tags[tag] = []), {
      type, parse,
      rank: info.rank == null ? 50 : info.rank
    }, (a, b) => a.rank - b.rank)
  })
  schema.registry("parseDOMStyle", (style, info, type) => {
    sortedInsert(styles[style] || (styles[style] = []), {
      type,
      parse: info.parse,
      rank: info.rank == null ? 50 : info.rank
    }, (a, b) => a.rank - b.rank)
  })
  return {tags, styles}
}

Paragraph.register("parseDOM", "p", {parse: "block"})

BlockQuote.register("parseDOM", "blockquote", {parse: "block"})

for (let i = 1; i <= 6; i++) Heading.registerComputed("parseDOM", "h" + i, type => {
  if (i <= type.maxLevel) return {
    parse: function(dom, state) { state.wrapIn(dom, this, {level: i}) }
  }
})

HorizontalRule.register("parseDOM", "hr", {parse: "block"})

CodeBlock.register("parseDOM", "pre", {parse: function(dom, state) {
  let params = dom.firstChild && /^code$/i.test(dom.firstChild.nodeName) && dom.firstChild.getAttribute("class")
  if (params && /fence/.test(params)) {
    let found = [], re = /(?:^|\s)lang-(\S+)/g, m
    while (m = re.test(params)) found.push(m[1])
    params = found.join(" ")
  } else {
    params = null
  }
  let text = dom.textContent
  state.insert(this, {params}, text ? [state.schema.text(text)] : [])
}})

BulletList.register("parseDOM", "ul", {parse: "block"})

OrderedList.register("parseDOM", "ol", {parse: function(dom, state) {
  let attrs = {order: dom.getAttribute("start") || 1}
  state.wrapIn(dom, this, attrs)
}})

ListItem.register("parseDOM", "li", {parse: "block"})

HardBreak.register("parseDOM", "br", {parse: function(_, state) {
  state.insert(this)
}})

Image.register("parseDOM", "img", {parse: function(dom, state) {
  state.insert(this, {
    src: dom.getAttribute("src"),
    title: dom.getAttribute("title") || null,
    alt: dom.getAttribute("alt") || null
  })
}})

// Inline style tokens

LinkMark.register("parseDOM", "a", {parse: function(dom, state) {
  let href = dom.getAttribute("href")
  if (!href) return false
  state.wrapMark(dom, this.create({href, title: dom.getAttribute("title")}))
}})

EmMark.register("parseDOM", "i", {parse: "mark"})
EmMark.register("parseDOM", "em", {parse: "mark"})
StrongMark.register("parseDOMStyle", "font-style", {parse: function(value, state, inner) {
  if (value == "italic") state.wrapMark(inner, this)
  else inner()
}})

StrongMark.register("parseDOM", "b", {parse: "mark"})
StrongMark.register("parseDOM", "strong", {parse: "mark"})
StrongMark.register("parseDOMStyle", "font-weight", {parse: function(value, state, inner) {
  if (value == "bold" || value == "bolder" || !/\D/.test(value) && +value >= 500) state.wrapMark(inner, this)
  else inner()
}})

CodeMark.register("parseDOM", "code", {parse: "mark"})
<commit_msg>Call correct RegExp method<commit_after>import {BlockQuote, OrderedList, BulletList, ListItem,
        HorizontalRule, Paragraph, Heading, CodeBlock, Image, HardBreak,
        EmMark, StrongMark, LinkMark, CodeMark, Node} from "../model"
import sortedInsert from "../util/sortedinsert"
import {defineSource} from "./register"

// :: (Schema, DOMNode, ?Object) → Node
// Parse document from the content of a DOM node. To pass an explicit
// parent document (for example, when not in a browser window
// environment, where we simply use the global document), pass it as
// the `document` property of `options`.
export function fromDOM(schema, dom, options) {
  if (!options) options = {}
  let context = new DOMParseState(schema, options.topNode || schema.node("doc"), options)
  let start = options.from ? dom.childNodes[options.from] : dom.firstChild
  let end = options.to != null && dom.childNodes[options.to] || null
  context.addAll(start, end, true)
  let doc
  while (context.stack.length) doc = context.leave()
  return doc
}

// ;; #path=DOMParseSpec #kind=interface
// To define the way [node](#NodeType) and [mark](#MarkType) types are
// parsed, you can associate one or more DOM parsing specifications to
// them using the [`register`](#SchemaItem.register) method with the
// `"parseDOM"` namespace, using the HTML node name (lowercase) as
// value name. Each of them defines a parsing strategy for a certain
// type of DOM node. When `"_"` is used as name, the parser is
// activated for all nodes.

// :: ?number #path=DOMParseSpec.rank
// The precedence of this parsing strategy. Should be a number between
// 0 and 100, which determines when this parser gets a chance relative
// to others that apply to the node (low ranks go first). Defaults to
// 50.

// :: union<string, (dom: DOMNode, state: DOMParseState) → ?bool> #path=DOMParseSpec.parse
// The function that, given a DOM node, parses it, updating the parse
// state. It should return (the exact value) `false` when it wants to
// indicate that it was not able to parse this node. This function is
// called in such a way that `this` is bound to the type that the
// parse spec was associated with.
//
// When this is set to the string `"block"`, the content of the DOM
// node is parsed as the content in a node of the type that this spec
// was associated with.
//
// When set to the string `"mark"`, the content of the DOM node is
// parsed with an instance of the mark that this spec was associated
// with added to their marks.

defineSource("dom", fromDOM)

// :: (Schema, string, ?Object) → Node
// Parses the HTML into a DOM, and then calls through to `fromDOM`.
export function fromHTML(schema, html, options) {
  let wrap = (options && options.document || window.document).createElement("div")
  wrap.innerHTML = html
  return fromDOM(schema, wrap, options)
}

defineSource("html", fromHTML)

const blockElements = {
  address: true, article: true, aside: true, blockquote: true, canvas: true,
  dd: true, div: true, dl: true, fieldset: true, figcaption: true, figure: true,
  footer: true, form: true, h1: true, h2: true, h3: true, h4: true, h5: true,
  h6: true, header: true, hgroup: true, hr: true, li: true, noscript: true, ol: true,
  output: true, p: true, pre: true, section: true, table: true, tfoot: true, ul: true
}

const ignoreElements = {
  head: true, noscript: true, object: true, script: true, style: true, title: true
}

const noMarks = []

// ;; A state object used to track context during a parse,
// and to expose methods to custom parsing functions.
class DOMParseState {
  constructor(schema, topNode, options) {
    // :: Object The options passed to this parse.
    this.options = options || {}
    // :: Schema The schema that we are parsing into.
    this.schema = schema
    this.stack = []
    this.marks = noMarks
    this.closing = false
    this.enter(topNode.type, topNode.attrs)
    let info = schemaInfo(schema)
    this.tagInfo = info.tags
    this.styleInfo = info.styles
  }

  get top() {
    return this.stack[this.stack.length - 1]
  }

  addDOM(dom) {
    if (dom.nodeType == 3) {
      let value = dom.nodeValue
      let top = this.top, last
      if (/\S/.test(value) || top.type.isTextblock) {
        if (!this.options.preserveWhitespace) {
          value = value.replace(/\s+/g, " ")
          // If this starts with whitespace, and there is either no node
          // before it or a node that ends with whitespace, strip the
          // leading space.
          if (/^\s/.test(value) &&
              (!(last = top.content[top.content.length - 1]) ||
               (last.type.name == "text" && /\s$/.test(last.text))))
            value = value.slice(1)
        }
        if (value)
          this.insertNode(this.schema.text(value, this.marks))
      }
    } else if (dom.nodeType != 1 || dom.hasAttribute("pm-ignore")) {
      // Ignore non-text non-element nodes
    } else {
      let style = dom.getAttribute("style")
      if (style) this.addElementWithStyles(parseStyles(style), dom)
      else this.addElement(dom)
    }
  }

  addElement(dom) {
    let name = dom.nodeName.toLowerCase()
    if (!this.parseNodeType(name, dom) && !ignoreElements.hasOwnProperty(name)) {
      this.addAll(dom.firstChild, null)
      if (blockElements.hasOwnProperty(name) && this.top.type == this.schema.defaultTextblockType())
        this.closing = true
    }
  }

  addElementWithStyles(styles, dom) {
    let wrappers = []
    for (let i = 0; i < styles.length; i += 2) {
      let parsers = this.styleInfo[styles[i]], value = styles[i + 1]
      if (parsers) for (let j = 0; j < parsers.length; j++)
        wrappers.push(parsers[j], value)
    }
    let next = (i) => {
      if (i == wrappers.length) {
        this.addElement(dom)
      } else {
        let parser = wrappers[i]
        parser.parse.call(parser.type, wrappers[i + 1], this, next.bind(null, i + 2))
      }
    }
    next(0)
  }

  tryParsers(parsers, dom) {
    if (parsers) for (let i = 0; i < parsers.length; i++) {
      let parser = parsers[i]
      if (parser.parse.call(parser.type, dom, this) !== false) return true
    }
  }

  parseNodeType(name, dom) {
    return this.tryParsers(this.tagInfo[name], dom) ||
      this.tryParsers(this.tagInfo._, dom)
  }

  addAll(from, to, sync) {
    let stack = sync && this.stack.slice()
    for (let dom = from; dom != to; dom = dom.nextSibling) {
      this.addDOM(dom)
      if (sync && blockElements.hasOwnProperty(dom.nodeName.toLowerCase()))
        this.sync(stack)
    }
  }

  doClose() {
    if (!this.closing || this.stack.length < 2) return
    let left = this.leave()
    this.enter(left.type, left.attrs)
    this.closing = false
  }

  insertNode(node) {
    if (this.top.type.canContain(node)) {
      this.doClose()
    } else {
      for (let i = this.stack.length - 1; i >= 0; i--) {
        let route = this.stack[i].type.findConnection(node.type)
        if (!route) continue
        if (i == this.stack.length - 1) {
          this.doClose()
        } else {
          while (this.stack.length > i + 1) this.leave()
        }
        for (let j = 0; j < route.length; j++)
          this.enter(route[j])
        if (this.marks.length) this.marks = noMarks
        break
      }
    }
    this.top.content.push(node)
    return node
  }

  // :: (DOMNode, NodeType, ?Object, [Node]) → Node
  // Insert a node of the given type, with the given content, based on
  // `dom`, at the current position in the document.
  insert(type, attrs, content) {
    return this.insertNode(type.createAutoFill(attrs, content, this.marks))
  }

  enter(type, attrs) {
    this.stack.push({type, attrs, content: []})
  }

  leave() {
    if (this.marks.length) this.marks = noMarks
    let top = this.stack.pop()
    let last = top.content[top.content.length - 1]
    if (!this.options.preserveWhitespace && last && last.isText && /\s$/.test(last.text))
      top.content[top.content.length - 1] = last.copy(last.text.slice(0, last.text.length - 1))
    let node = top.type.createAutoFill(top.attrs, top.content)
    if (this.stack.length) this.insertNode(node)
    return node
  }

  sync(stack) {
    while (this.stack.length > stack.length) this.leave()
    for (;;) {
      let n = this.stack.length - 1, one = this.stack[n], two = stack[n]
      if (one.type == two.type && Node.sameAttrs(one.attrs, two.attrs)) break
      this.leave()
    }
    while (stack.length > this.stack.length) {
      let add = stack[this.stack.length]
      this.enter(add.type, add.attrs)
    }
    if (this.marks.length) this.marks = noMarks
    this.closing = false
  }

  // :: (DOMNode, NodeType, ?Object)
  // Parse the contents of `dom` as children of a node of the given
  // type.
  wrapIn(dom, type, attrs) {
    this.enter(type, attrs)
    this.addAll(dom.firstChild, null, true)
    this.leave()
  }

  // :: (DOMNode, Mark)
  // Parse the contents of `dom`, with `mark` added to the set of
  // current marks.
  wrapMark(inner, mark) {
    let old = this.marks
    this.marks = (mark.instance || mark).addToSet(old)
    if (inner.call) inner()
    else this.addAll(inner.firstChild, null)
    this.marks = old
  }
}

function parseStyles(style) {
  let re = /\s*([\w-]+)\s*:\s*([^;]+)/g, m, result = []
  while (m = re.exec(style)) result.push(m[1], m[2].trim())
  return result
}

function schemaInfo(schema) {
  return schema.cached.parseDOMInfo || (schema.cached.parseDOMInfo = summarizeSchemaInfo(schema))
}

function summarizeSchemaInfo(schema) {
  let tags = Object.create(null), styles = Object.create(null)
  tags._ = []
  schema.registry("parseDOM", (tag, info, type) => {
    let parse = info.parse
    if (parse == "block")
      parse = function(dom, state) { state.wrapIn(dom, this) }
    else if (parse == "mark")
      parse = function(dom, state) { state.wrapMark(dom, this) }
    sortedInsert(tags[tag] || (tags[tag] = []), {
      type, parse,
      rank: info.rank == null ? 50 : info.rank
    }, (a, b) => a.rank - b.rank)
  })
  schema.registry("parseDOMStyle", (style, info, type) => {
    sortedInsert(styles[style] || (styles[style] = []), {
      type,
      parse: info.parse,
      rank: info.rank == null ? 50 : info.rank
    }, (a, b) => a.rank - b.rank)
  })
  return {tags, styles}
}

Paragraph.register("parseDOM", "p", {parse: "block"})

BlockQuote.register("parseDOM", "blockquote", {parse: "block"})

for (let i = 1; i <= 6; i++) Heading.registerComputed("parseDOM", "h" + i, type => {
  if (i <= type.maxLevel) return {
    parse: function(dom, state) { state.wrapIn(dom, this, {level: i}) }
  }
})

HorizontalRule.register("parseDOM", "hr", {parse: "block"})

CodeBlock.register("parseDOM", "pre", {parse: function(dom, state) {
  let params = dom.firstChild && /^code$/i.test(dom.firstChild.nodeName) && dom.firstChild.getAttribute("class")
  if (params && /fence/.test(params)) {
    let found = [], re = /(?:^|\s)lang-(\S+)/g, m
    while (m = re.exec(params)) found.push(m[1])
    params = found.join(" ")
  } else {
    params = null
  }
  let text = dom.textContent
  state.insert(this, {params}, text ? [state.schema.text(text)] : [])
}})

BulletList.register("parseDOM", "ul", {parse: "block"})

OrderedList.register("parseDOM", "ol", {parse: function(dom, state) {
  let attrs = {order: dom.getAttribute("start") || 1}
  state.wrapIn(dom, this, attrs)
}})

ListItem.register("parseDOM", "li", {parse: "block"})

HardBreak.register("parseDOM", "br", {parse: function(_, state) {
  state.insert(this)
}})

Image.register("parseDOM", "img", {parse: function(dom, state) {
  state.insert(this, {
    src: dom.getAttribute("src"),
    title: dom.getAttribute("title") || null,
    alt: dom.getAttribute("alt") || null
  })
}})

// Inline style tokens

LinkMark.register("parseDOM", "a", {parse: function(dom, state) {
  let href = dom.getAttribute("href")
  if (!href) return false
  state.wrapMark(dom, this.create({href, title: dom.getAttribute("title")}))
}})

EmMark.register("parseDOM", "i", {parse: "mark"})
EmMark.register("parseDOM", "em", {parse: "mark"})
StrongMark.register("parseDOMStyle", "font-style", {parse: function(value, state, inner) {
  if (value == "italic") state.wrapMark(inner, this)
  else inner()
}})

StrongMark.register("parseDOM", "b", {parse: "mark"})
StrongMark.register("parseDOM", "strong", {parse: "mark"})
StrongMark.register("parseDOMStyle", "font-weight", {parse: function(value, state, inner) {
  if (value == "bold" || value == "bolder" || !/\D/.test(value) && +value >= 500) state.wrapMark(inner, this)
  else inner()
}})

CodeMark.register("parseDOM", "code", {parse: "mark"})
<|endoftext|>


<commit_before>import * as I          from 'immutable';
import * as fw         from './freeWords';
import * as generators from '../common/generators';

import { Partition } from '../common/unionFind';

const range = (from, to) => {
  if (to < from)
    return new Array(from - to).fill(0).map((_, i) => from - i);
  else
    return new Array(to - from).fill(0).map((_, i) => from + i);
};

const _joinInTable = (t, a, b, g) => t.setIn([a, g], b).setIn([b, -g], a);

const identify = (table, part, a, b) => {
  part = part.clone();
  table = table.asMutable();

  const queue = [[a, b]];

  while (queue.length) {
    const [a, b] = queue.shift().map(x => part.find(x));

    if (a != b) {
      part.union(a, b);

      for (const [g, bg] of table.get(b)) {
        const ag = table.getIn([a, g]);
        if (ag == null)
          table.setIn([a, g], bg);
        else if (part.find(ag) != part.find(bg))
          queue.push([ag, bg]);
      }
      table.set(b, table.get(a));
    }
  }

  return { table: table.asImmutable(), part };
};

const scan = (table, w, start, limit) => {
  let row = start;
  let i = 0;

  while (i < limit) {
    const next = table.getIn([row, w.get(i)]);
    if (next == null)
      break;
    else {
      ++i;
      row = next;
    }
  }

  return { row, index: i };
};

const scanAndIdentify = (table, part, w, start) => {
  const n = w.size;
  const { row: head, index: i } = scan(table, w, start, n);
  const { row: tail, index: j } = scan(table, fw.inverse(w), start, n - i);

  if (i + j == n - 1) {
    table = _joinInTable(table, head, tail, w.get(i));
    return { table, part, next: head };
  }
  else if (i + j == n && head != tail)
    return identify(table, part, head, tail);
  else
    return { table, part };
};

const compressed = (table, part) => {
  const toIdx = {};
  let i = 0;
  for (const k of table.keySeq()) {
    if (part.find(k) == k) {
      toIdx[k] = i;
      ++i;
    }
  }

  const canon = a => toIdx[part.find(a)];

  return table.toMap()
    .filter((r, k) => toIdx[k] != null)
    .mapKeys(canon)
    .map(row => row.map(canon));
};

const _expandGenerators = nrGens =>
  range(1, nrGens+1).concat(range(-1, -(nrGens+1)));

const _expandRelators = relators =>
  I.Set(I.List(relators).flatMap(fw.relatorPermutations));

export const cosetTable = (nrGens, relators, subgroupGens) => {
  const gens = _expandGenerators(nrGens);
  const rels = _expandRelators(relators);

  let table = I.List([I.Map()]);
  let part = new Partition();

  for (let i = 0; i < table.size; ++i) {
    if (i != part.find(i))
      continue;

    for (const g of gens) {
      if (table.getIn([i, g]) == null) {
        const n = table.size;
        if (n >= 10000)
          throw new Error('maximum coset table size reached');

        let t = { table: _joinInTable(table, i, n, g), part };
        for (const w of rels)
          t = scanAndIdentify(t.table, t.part, w, n);
        for (const w of subgroupGens)
          t = scanAndIdentify(t.table, t.part, fw.word(w), part.find(0));

        table = t.table;
        part = t.part;
      }
    }
  }

  return compressed(table, part);
};

export const cosetRepresentatives = table => {
  const queue = [0];
  const reps = [fw.empty];

  while (queue.length) {
    const i = queue.shift();
    const w = reps[i];

    for (const [g, k] of table.get(i).filter(g => reps[g] == null)) {
      reps[k] = fw.product([w, [g]]);
      queue.push(k);
    }
  }

  return reps;
};

const _firstFreeInTable = (table, gens) => {
  for (let k = 0; k < table.size; ++k) {
    for (const g of gens) {
      if (table.getIn([k, g]) == null)
        return [k, g];
    }
  }
};

const _scanRecursively = (rels, table, index) => {
  const p = () => new Partition();
  const q = [index];

  while (q.length) {
    const row = q.shift();

    for (const rel of rels) {
      const { table: t, part, next } = scanAndIdentify(table, p(), rel, row);

      if (part.isTrivial()) {
        table = t;
        if (next != null)
          q.push(next);
      }
      else
        return;
    }
  }

  return table;
};

const _potentialChildren = (table, gens, rels, maxCosets) => {
  const [k, g] = _firstFreeInTable(table, gens) || [];
  const result = [];

  if (k != null) {
    const ginv = -g;
    const limit = Math.min(table.size + 1, maxCosets);

    for (let pos = k; pos < limit; ++pos) {
      if (table.getIn([pos, ginv]) == null) {
        const t = _scanRecursively(rels, _joinInTable(table, k, pos, g), k);
        if (t != null)
          result.push(t);
      }
    }
  }

  return result;
};

const _compareRenumberedFom = (table, gens, start) => {
  const n2o = [start];
  const o2n = { [start]: 0 };

  for (let row = 0; row < table.size; ++row) {
    if (row >= n2o.length)
      throw new Error("coset table is not transitive");

    for (const g of gens) {
      const t = table.getIn([n2o[row], g]);
      if (t != null && o2n[t] == null) {
        o2n[t] = n2o.length;
        n2o.push(t);
      }

      const nval = o2n[t];
      const oval = table.getIn([row, g]);
      if (oval != nval)
        return oval == null ? -1 : nval == null ? 1 : nval - oval;
    }
  }

  return 0;
};

const _isCanonical = (table, gens) => range(1, table.size)
  .every(start => _compareRenumberedFom(table, gens, start) >= 0);

export const tables = (nrGens, relators, maxCosets) => {
  const gens = _expandGenerators(nrGens);
  const rels = _expandRelators(relators);
  const isFull = t => _firstFreeInTable(t, gens) == null;

  return generators.backtracker({
    root: I.List([I.Map()]),
    extract(table) { return isFull(table) ? table : null },
    children(table) {
      return _potentialChildren(table, gens, rels, maxCosets)
        .filter(t => !t.isEmpty() && _isCanonical(t, gens));
    }
  });
};

const _inducedTable = (gens, img, img0) => {
  const table = I.List([I.Map()]).asMutable();
  const o2n = I.Map([[img0, 0]]).asMutable();
  const n2o = I.Map([[0, img0]]).asMutable();
  let i = 0;

  while (i < table.size) {
    gens.forEach(g => {
      const k = img(n2o.get(i), g);
      const n = o2n.has(k) ? o2n.get(k) : table.size;
      o2n.set(k, n);
      n2o.set(n, k);
      _joinInTable(table, i, n, g);
    });
    ++i;
  }

  return table.asImmutable();
};

export const intersectionTable = (tableA, tableB) => {
  return _inducedTable(
    (tableA.first() || I.Map()).keySeq(),
    (es, g) => I.List([tableA.getIn([es.get(0), g]),
                       tableB.getIn([es.get(1), g])]),
    I.List([0, 0])
  );
};

export const coreTable = base =>
  _inducedTable(
    (base.first() || I.Map()).keySeq(),
    (es, g) => es.map(e => base.getIn([e, g])),
    base.keySeq()
  );

const _sgn = x => (x > 0) - (x < 0);
const _sum = a => a.reduce((x, y) => x + y, 0);

export const relatorAsVector = (rel, nrgens) => {
  const counts = rel.groupBy(Math.abs).map(a => _sum(a.map(_sgn)));
  return I.List(range(1, nrgens+1).map(i => counts.get(i) || 0));
};

export const relatorMatrix = (nrgens, relators) =>
  I.List(relators).map(rel => relatorAsVector(rel, nrgens));

if (require.main == module) {
  const test = table => {
    const reps = cosetRepresentatives(table);
    console.log(I.fromJS(reps), Object.keys(reps).length);
  };

  const base = cosetTable(
    3,
    [[1,1], [2,2], [3,3], [1,2,1,2,1,2], [1,3,1,3], fw.raisedTo(3, [2,3])],
    [[1,2]]);

  test(base);
  test(coreTable(base));

  console.log(_expandGenerators(4));
  console.log(_expandRelators([[1,2,-3]]));

  for (const x of generators.results(tables(2, [[1,1],[2,2],[1,2,1,2]], 8)))
    console.log(JSON.stringify(x));
}
<commit_msg>implement _expandRelators() in plain JS<commit_after>import * as I          from 'immutable';
import * as fw         from './freeWords';
import * as generators from '../common/generators';

import { Partition } from '../common/unionFind';

const range = (from, to) => {
  if (to < from)
    return new Array(from - to).fill(0).map((_, i) => from - i);
  else
    return new Array(to - from).fill(0).map((_, i) => from + i);
};

const _joinInTable = (t, a, b, g) => t.setIn([a, g], b).setIn([b, -g], a);

const identify = (table, part, a, b) => {
  part = part.clone();
  table = table.asMutable();

  const queue = [[a, b]];

  while (queue.length) {
    const [a, b] = queue.shift().map(x => part.find(x));

    if (a != b) {
      part.union(a, b);

      for (const [g, bg] of table.get(b)) {
        const ag = table.getIn([a, g]);
        if (ag == null)
          table.setIn([a, g], bg);
        else if (part.find(ag) != part.find(bg))
          queue.push([ag, bg]);
      }
      table.set(b, table.get(a));
    }
  }

  return { table: table.asImmutable(), part };
};

const scan = (table, w, start, limit) => {
  let row = start;
  let i = 0;

  while (i < limit) {
    const next = table.getIn([row, w.get(i)]);
    if (next == null)
      break;
    else {
      ++i;
      row = next;
    }
  }

  return { row, index: i };
};

const scanAndIdentify = (table, part, w, start) => {
  const n = w.size;
  const { row: head, index: i } = scan(table, w, start, n);
  const { row: tail, index: j } = scan(table, fw.inverse(w), start, n - i);

  if (i + j == n - 1) {
    table = _joinInTable(table, head, tail, w.get(i));
    return { table, part, next: head };
  }
  else if (i + j == n && head != tail)
    return identify(table, part, head, tail);
  else
    return { table, part };
};

const compressed = (table, part) => {
  const toIdx = {};
  let i = 0;
  for (const k of table.keySeq()) {
    if (part.find(k) == k) {
      toIdx[k] = i;
      ++i;
    }
  }

  const canon = a => toIdx[part.find(a)];

  return table.toMap()
    .filter((r, k) => toIdx[k] != null)
    .mapKeys(canon)
    .map(row => row.map(canon));
};

const _expandGenerators = nrGens =>
  range(1, nrGens+1).concat(range(-1, -(nrGens+1)));

const _insertInOrderedSet = (elm, set, cmp) => {
  let i = 0;
  while (i < set.length && cmp(elm, set[i]) < 0)
    ++i;
  if (i >= set.length || cmp(elm, set[i]) != 0)
    set.splice(i, 0, elm);
};

const _expandRelators = relators => {
  const out = [];
  for (const rel of relators) {
    for (const w of fw.relatorPermutations(rel))
      _insertInOrderedSet(w, out, fw.compare);
  }
  return out;
};

export const cosetTable = (nrGens, relators, subgroupGens) => {
  const gens = _expandGenerators(nrGens);
  const rels = _expandRelators(relators);

  let table = I.List([I.Map()]);
  let part = new Partition();

  for (let i = 0; i < table.size; ++i) {
    if (i != part.find(i))
      continue;

    for (const g of gens) {
      if (table.getIn([i, g]) == null) {
        const n = table.size;
        if (n >= 10000)
          throw new Error('maximum coset table size reached');

        let t = { table: _joinInTable(table, i, n, g), part };
        for (const w of rels)
          t = scanAndIdentify(t.table, t.part, w, n);
        for (const w of subgroupGens)
          t = scanAndIdentify(t.table, t.part, fw.word(w), part.find(0));

        table = t.table;
        part = t.part;
      }
    }
  }

  return compressed(table, part);
};

export const cosetRepresentatives = table => {
  const queue = [0];
  const reps = [fw.empty];

  while (queue.length) {
    const i = queue.shift();
    const w = reps[i];

    for (const [g, k] of table.get(i).filter(g => reps[g] == null)) {
      reps[k] = fw.product([w, [g]]);
      queue.push(k);
    }
  }

  return reps;
};

const _firstFreeInTable = (table, gens) => {
  for (let k = 0; k < table.size; ++k) {
    for (const g of gens) {
      if (table.getIn([k, g]) == null)
        return [k, g];
    }
  }
};

const _scanRecursively = (rels, table, index) => {
  const p = () => new Partition();
  const q = [index];

  while (q.length) {
    const row = q.shift();

    for (const rel of rels) {
      const { table: t, part, next } = scanAndIdentify(table, p(), rel, row);

      if (part.isTrivial()) {
        table = t;
        if (next != null)
          q.push(next);
      }
      else
        return;
    }
  }

  return table;
};

const _potentialChildren = (table, gens, rels, maxCosets) => {
  const [k, g] = _firstFreeInTable(table, gens) || [];
  const result = [];

  if (k != null) {
    const ginv = -g;
    const limit = Math.min(table.size + 1, maxCosets);

    for (let pos = k; pos < limit; ++pos) {
      if (table.getIn([pos, ginv]) == null) {
        const t = _scanRecursively(rels, _joinInTable(table, k, pos, g), k);
        if (t != null)
          result.push(t);
      }
    }
  }

  return result;
};

const _compareRenumberedFom = (table, gens, start) => {
  const n2o = [start];
  const o2n = { [start]: 0 };

  for (let row = 0; row < table.size; ++row) {
    if (row >= n2o.length)
      throw new Error("coset table is not transitive");

    for (const g of gens) {
      const t = table.getIn([n2o[row], g]);
      if (t != null && o2n[t] == null) {
        o2n[t] = n2o.length;
        n2o.push(t);
      }

      const nval = o2n[t];
      const oval = table.getIn([row, g]);
      if (oval != nval)
        return oval == null ? -1 : nval == null ? 1 : nval - oval;
    }
  }

  return 0;
};

const _isCanonical = (table, gens) => range(1, table.size)
  .every(start => _compareRenumberedFom(table, gens, start) >= 0);

export const tables = (nrGens, relators, maxCosets) => {
  const gens = _expandGenerators(nrGens);
  const rels = _expandRelators(relators);
  const isFull = t => _firstFreeInTable(t, gens) == null;

  return generators.backtracker({
    root: I.List([I.Map()]),
    extract(table) { return isFull(table) ? table : null },
    children(table) {
      return _potentialChildren(table, gens, rels, maxCosets)
        .filter(t => !t.isEmpty() && _isCanonical(t, gens));
    }
  });
};

const _inducedTable = (gens, img, img0) => {
  const table = I.List([I.Map()]).asMutable();
  const o2n = I.Map([[img0, 0]]).asMutable();
  const n2o = I.Map([[0, img0]]).asMutable();
  let i = 0;

  while (i < table.size) {
    gens.forEach(g => {
      const k = img(n2o.get(i), g);
      const n = o2n.has(k) ? o2n.get(k) : table.size;
      o2n.set(k, n);
      n2o.set(n, k);
      _joinInTable(table, i, n, g);
    });
    ++i;
  }

  return table.asImmutable();
};

export const intersectionTable = (tableA, tableB) => {
  return _inducedTable(
    (tableA.first() || I.Map()).keySeq(),
    (es, g) => I.List([tableA.getIn([es.get(0), g]),
                       tableB.getIn([es.get(1), g])]),
    I.List([0, 0])
  );
};

export const coreTable = base =>
  _inducedTable(
    (base.first() || I.Map()).keySeq(),
    (es, g) => es.map(e => base.getIn([e, g])),
    base.keySeq()
  );

const _sgn = x => (x > 0) - (x < 0);
const _sum = a => a.reduce((x, y) => x + y, 0);

export const relatorAsVector = (rel, nrgens) => {
  const counts = rel.groupBy(Math.abs).map(a => _sum(a.map(_sgn)));
  return I.List(range(1, nrgens+1).map(i => counts.get(i) || 0));
};

export const relatorMatrix = (nrgens, relators) =>
  I.List(relators).map(rel => relatorAsVector(rel, nrgens));

if (require.main == module) {
  const test = table => {
    const reps = cosetRepresentatives(table);
    console.log(I.fromJS(reps), Object.keys(reps).length);
  };

  const base = cosetTable(
    3,
    [[1,1], [2,2], [3,3], [1,2,1,2,1,2], [1,3,1,3], fw.raisedTo(3, [2,3])],
    [[1,2]]);

  test(base);
  test(coreTable(base));

  console.log(_expandGenerators(4));
  console.log(_expandRelators([[1,2,-3]]));

  for (const x of generators.results(tables(2, [[1,1],[2,2],[1,2,1,2]], 8)))
    console.log(JSON.stringify(x));
}
<|endoftext|>