<commit_before>// @flow
jest.mock("../../src/components/notebook-menu/extra-handlers");

import React from "react";
import renderer from "react-test-renderer";
import * as Immutable from "immutable";
import * as extraHandlers from "../../src/components/notebook-menu/extra-handlers";
import { shallow, mount } from "enzyme";
import { PureNotebookMenu } from "../../src/components/notebook-menu";
import {
  MENU_ITEM_ACTIONS,
  MENUS
} from "../../src/components/notebook-menu/constants";

describe("PureNotebookMenu ", () => {
  describe("snapshots", () => {
    test("renders the default", () => {
      const component = renderer.create(<PureNotebookMenu />);
      let tree = component.toJSON();
      expect(tree).toMatchSnapshot();
    });
  });
  describe("shallow", () => {
    test("renders the default", () => {
      const wrapper = shallow(<PureNotebookMenu />);
      expect(wrapper).not.toBeNull();
    });
  });
  describe("mount", () => {
    test("renders the default", () => {
      const wrapper = mount(<PureNotebookMenu />);
      expect(wrapper).not.toBeNull();
    });
    test("calls appropriate handlers on click", () => {
      const props = {
        // action functions
        executeCell: jest.fn(),
        cutCell: jest.fn(),
        copyCell: jest.fn(),
        pasteCell: jest.fn(),
        createCodeCell: jest.fn(),
        createMarkdownCell: jest.fn(),
        mergeCellAfter: jest.fn(),
        setCellTypeCode: jest.fn(),
        setCellTypeMarkdown: jest.fn(),
        setTheme: jest.fn(),
        saveNotebook: jest.fn(),
        openAboutModal: jest.fn(),

        // document state (we mock out the implementation, so these are just
        // dummy variables.
        cellFocused: "fake",
        cellMap: Immutable.Map(),
        cellOrder: Immutable.List(),
        notebook: Immutable.Map(),
        filename: "fake.ipynb",

        // menu props, note that we force all menus to be open to click.
        defaultOpenKeys: Object.values(MENUS)
      };
      const wrapper = mount(<PureNotebookMenu {...props} />);

      // Test that we call executeAllCells
      const executeAllCellsItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.EXECUTE_ALL_CELLS })
        .first();
      expect(extraHandlers.executeAllCells).not.toHaveBeenCalled();
      executeAllCellsItem.simulate("click");
      expect(extraHandlers.executeAllCells).toHaveBeenCalledTimes(1);
      expect(extraHandlers.executeAllCells).toHaveBeenCalledWith(
        props.executeCell,
        props.cellMap,
        props.cellOrder
      );

      const executeAllCellsBelowItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.EXECUTE_ALL_CELLS_BELOW })
        .first();
      expect(extraHandlers.executeAllCellsBelow).not.toHaveBeenCalled();
      executeAllCellsBelowItem.simulate("click");
      expect(extraHandlers.executeAllCellsBelow).toHaveBeenCalledTimes(1);
      expect(extraHandlers.executeAllCellsBelow).toHaveBeenCalledWith(
        props.executeCell,
        props.cellMap,
        props.cellOrder,
        props.cellFocused
      );

      const downloadNotebookItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.DOWNLOAD_NOTEBOOK })
        .first();
      expect(extraHandlers.downloadNotebook).not.toHaveBeenCalled();
      downloadNotebookItem.simulate("click");
      expect(extraHandlers.downloadNotebook).toHaveBeenCalledTimes(1);
      expect(extraHandlers.downloadNotebook).toHaveBeenCalledWith(
        props.notebook,
        props.filename
      );

      const cutCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.CUT_CELL })
        .first();
      expect(props.cutCell).not.toHaveBeenCalled();
      cutCellItem.simulate("click");
      expect(props.cutCell).toHaveBeenCalledTimes(1);
      expect(props.cutCell).toHaveBeenCalledWith(props.cellFocused);

      const copyCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.COPY_CELL })
        .first();
      expect(props.copyCell).not.toHaveBeenCalled();
      copyCellItem.simulate("click");
      expect(props.copyCell).toHaveBeenCalledTimes(1);
      expect(props.copyCell).toHaveBeenCalledWith(props.cellFocused);

      const pasteCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.PASTE_CELL })
        .first();
      expect(props.pasteCell).not.toHaveBeenCalled();
      pasteCellItem.simulate("click");
      expect(props.pasteCell).toHaveBeenCalledTimes(1);
      expect(props.pasteCell).toHaveBeenCalledWith();

      const mergeCellAfterItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.MERGE_CELL_AFTER })
        .first();
      expect(props.mergeCellAfter).not.toHaveBeenCalled();
      mergeCellAfterItem.simulate("click");
      expect(props.mergeCellAfter).toHaveBeenCalledTimes(1);
      expect(props.mergeCellAfter).toHaveBeenCalledWith(props.cellFocused);

      const createMarkdownCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.CREATE_MARKDOWN_CELL })
        .first();
      expect(props.createMarkdownCell).not.toHaveBeenCalled();
      createMarkdownCellItem.simulate("click");
      expect(props.createMarkdownCell).toHaveBeenCalledTimes(1);
      expect(props.createMarkdownCell).toHaveBeenCalledWith(props.cellFocused);

      const createCodeCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.CREATE_CODE_CELL })
        .first();
      expect(props.createCodeCell).not.toHaveBeenCalled();
      createCodeCellItem.simulate("click");
      expect(props.createCodeCell).toHaveBeenCalledTimes(1);
      expect(props.createCodeCell).toHaveBeenCalledWith(props.cellFocused);

      const setCellTypeCodeItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SET_CELL_TYPE_CODE })
        .first();
      expect(props.setCellTypeCode).not.toHaveBeenCalled();
      setCellTypeCodeItem.simulate("click");
      expect(props.setCellTypeCode).toHaveBeenCalledTimes(1);
      expect(props.setCellTypeCode).toHaveBeenCalledWith(props.cellFocused);

      const setCellTypeMarkdownItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SET_CELL_TYPE_MARKDOWN })
        .first();
      expect(props.setCellTypeMarkdown).not.toHaveBeenCalled();
      setCellTypeMarkdownItem.simulate("click");
      expect(props.setCellTypeMarkdown).toHaveBeenCalledTimes(1);
      expect(props.setCellTypeMarkdown).toHaveBeenCalledWith(props.cellFocused);

      const setThemeLightItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SET_THEME_LIGHT })
        .first();
      expect(props.setTheme).not.toHaveBeenCalled();
      setThemeLightItem.simulate("click");
      expect(props.setTheme).toHaveBeenCalledTimes(1);
      expect(props.setTheme).toHaveBeenCalledWith("light");

      props.setTheme.mockClear();
      const setThemeDarkItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SET_THEME_DARK })
        .first();
      expect(props.setTheme).not.toHaveBeenCalled();
      setThemeDarkItem.simulate("click");
      expect(props.setTheme).toHaveBeenCalledTimes(1);
      expect(props.setTheme).toHaveBeenCalledWith("dark");

      const saveNotebookItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SAVE_NOTEBOOK })
        .first();
      expect(props.saveNotebook).not.toHaveBeenCalled();
      saveNotebookItem.simulate("click");
      expect(props.saveNotebook).toHaveBeenCalledTimes(1);

      const openAboutItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.OPEN_ABOUT })
        .first();
      expect(props.openAboutModal).not.toHaveBeenCalled();
      openAboutItem.simulate("click");
      expect(props.openAboutModal).toHaveBeenCalledTimes(1);
    });
  });
});
<commit_msg>create test for interruptKernel menu item<commit_after>// @flow
jest.mock("../../src/components/notebook-menu/extra-handlers");

import React from "react";
import renderer from "react-test-renderer";
import * as Immutable from "immutable";
import * as extraHandlers from "../../src/components/notebook-menu/extra-handlers";
import { shallow, mount } from "enzyme";
import { PureNotebookMenu } from "../../src/components/notebook-menu";
import {
  MENU_ITEM_ACTIONS,
  MENUS
} from "../../src/components/notebook-menu/constants";

describe("PureNotebookMenu ", () => {
  describe("snapshots", () => {
    test("renders the default", () => {
      const component = renderer.create(<PureNotebookMenu />);
      let tree = component.toJSON();
      expect(tree).toMatchSnapshot();
    });
  });
  describe("shallow", () => {
    test("renders the default", () => {
      const wrapper = shallow(<PureNotebookMenu />);
      expect(wrapper).not.toBeNull();
    });
  });
  describe("mount", () => {
    test("renders the default", () => {
      const wrapper = mount(<PureNotebookMenu />);
      expect(wrapper).not.toBeNull();
    });
    test("calls appropriate handlers on click", () => {
      const props = {
        // action functions
        executeCell: jest.fn(),
        cutCell: jest.fn(),
        copyCell: jest.fn(),
        pasteCell: jest.fn(),
        createCodeCell: jest.fn(),
        createMarkdownCell: jest.fn(),
        mergeCellAfter: jest.fn(),
        setCellTypeCode: jest.fn(),
        setCellTypeMarkdown: jest.fn(),
        setTheme: jest.fn(),
        saveNotebook: jest.fn(),
        openAboutModal: jest.fn(),
        interruptKernel: jest.fn(),

        // document state (we mock out the implementation, so these are just
        // dummy variables.
        cellFocused: "fake",
        cellMap: Immutable.Map(),
        cellOrder: Immutable.List(),
        notebook: Immutable.Map(),
        filename: "fake.ipynb",

        // menu props, note that we force all menus to be open to click.
        defaultOpenKeys: Object.values(MENUS)
      };
      const wrapper = mount(<PureNotebookMenu {...props} />);

      // Test that we call executeAllCells
      const executeAllCellsItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.EXECUTE_ALL_CELLS })
        .first();
      expect(extraHandlers.executeAllCells).not.toHaveBeenCalled();
      executeAllCellsItem.simulate("click");
      expect(extraHandlers.executeAllCells).toHaveBeenCalledTimes(1);
      expect(extraHandlers.executeAllCells).toHaveBeenCalledWith(
        props.executeCell,
        props.cellMap,
        props.cellOrder
      );

      const executeAllCellsBelowItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.EXECUTE_ALL_CELLS_BELOW })
        .first();
      expect(extraHandlers.executeAllCellsBelow).not.toHaveBeenCalled();
      executeAllCellsBelowItem.simulate("click");
      expect(extraHandlers.executeAllCellsBelow).toHaveBeenCalledTimes(1);
      expect(extraHandlers.executeAllCellsBelow).toHaveBeenCalledWith(
        props.executeCell,
        props.cellMap,
        props.cellOrder,
        props.cellFocused
      );

      const downloadNotebookItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.DOWNLOAD_NOTEBOOK })
        .first();
      expect(extraHandlers.downloadNotebook).not.toHaveBeenCalled();
      downloadNotebookItem.simulate("click");
      expect(extraHandlers.downloadNotebook).toHaveBeenCalledTimes(1);
      expect(extraHandlers.downloadNotebook).toHaveBeenCalledWith(
        props.notebook,
        props.filename
      );

      const cutCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.CUT_CELL })
        .first();
      expect(props.cutCell).not.toHaveBeenCalled();
      cutCellItem.simulate("click");
      expect(props.cutCell).toHaveBeenCalledTimes(1);
      expect(props.cutCell).toHaveBeenCalledWith(props.cellFocused);

      const copyCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.COPY_CELL })
        .first();
      expect(props.copyCell).not.toHaveBeenCalled();
      copyCellItem.simulate("click");
      expect(props.copyCell).toHaveBeenCalledTimes(1);
      expect(props.copyCell).toHaveBeenCalledWith(props.cellFocused);

      const pasteCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.PASTE_CELL })
        .first();
      expect(props.pasteCell).not.toHaveBeenCalled();
      pasteCellItem.simulate("click");
      expect(props.pasteCell).toHaveBeenCalledTimes(1);
      expect(props.pasteCell).toHaveBeenCalledWith();

      const mergeCellAfterItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.MERGE_CELL_AFTER })
        .first();
      expect(props.mergeCellAfter).not.toHaveBeenCalled();
      mergeCellAfterItem.simulate("click");
      expect(props.mergeCellAfter).toHaveBeenCalledTimes(1);
      expect(props.mergeCellAfter).toHaveBeenCalledWith(props.cellFocused);

      const createMarkdownCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.CREATE_MARKDOWN_CELL })
        .first();
      expect(props.createMarkdownCell).not.toHaveBeenCalled();
      createMarkdownCellItem.simulate("click");
      expect(props.createMarkdownCell).toHaveBeenCalledTimes(1);
      expect(props.createMarkdownCell).toHaveBeenCalledWith(props.cellFocused);

      const createCodeCellItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.CREATE_CODE_CELL })
        .first();
      expect(props.createCodeCell).not.toHaveBeenCalled();
      createCodeCellItem.simulate("click");
      expect(props.createCodeCell).toHaveBeenCalledTimes(1);
      expect(props.createCodeCell).toHaveBeenCalledWith(props.cellFocused);

      const setCellTypeCodeItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SET_CELL_TYPE_CODE })
        .first();
      expect(props.setCellTypeCode).not.toHaveBeenCalled();
      setCellTypeCodeItem.simulate("click");
      expect(props.setCellTypeCode).toHaveBeenCalledTimes(1);
      expect(props.setCellTypeCode).toHaveBeenCalledWith(props.cellFocused);

      const setCellTypeMarkdownItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SET_CELL_TYPE_MARKDOWN })
        .first();
      expect(props.setCellTypeMarkdown).not.toHaveBeenCalled();
      setCellTypeMarkdownItem.simulate("click");
      expect(props.setCellTypeMarkdown).toHaveBeenCalledTimes(1);
      expect(props.setCellTypeMarkdown).toHaveBeenCalledWith(props.cellFocused);

      const setThemeLightItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SET_THEME_LIGHT })
        .first();
      expect(props.setTheme).not.toHaveBeenCalled();
      setThemeLightItem.simulate("click");
      expect(props.setTheme).toHaveBeenCalledTimes(1);
      expect(props.setTheme).toHaveBeenCalledWith("light");

      props.setTheme.mockClear();
      const setThemeDarkItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SET_THEME_DARK })
        .first();
      expect(props.setTheme).not.toHaveBeenCalled();
      setThemeDarkItem.simulate("click");
      expect(props.setTheme).toHaveBeenCalledTimes(1);
      expect(props.setTheme).toHaveBeenCalledWith("dark");

      const saveNotebookItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.SAVE_NOTEBOOK })
        .first();
      expect(props.saveNotebook).not.toHaveBeenCalled();
      saveNotebookItem.simulate("click");
      expect(props.saveNotebook).toHaveBeenCalledTimes(1);

      const openAboutItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.OPEN_ABOUT })
        .first();
      expect(props.openAboutModal).not.toHaveBeenCalled();
      openAboutItem.simulate("click");
      expect(props.openAboutModal).toHaveBeenCalledTimes(1);

      const interruptKernelItem = wrapper
        .find({ eventKey: MENU_ITEM_ACTIONS.INTERRUPT_KERNEL })
        .first();
      expect(props.interruptKernel).not.toHaveBeenCalled();
      interruptKernelItem.simulate("click");
      expect(props.interruptKernel).toHaveBeenCalledTimes(1);
    });
  });
});
<|endoftext|>


<commit_before>import React from 'react';
import { Link } from 'react-router-dom';
import { Img } from 'normalized-styled-components';
import styled from 'styled-components';
import remcalc from 'remcalc';
import unitcalc from 'unitcalc';

import Logo from '@assets/triton_logo.png';
import { Col, Row } from 'react-styled-flexboxgrid';

const StyledHeader = styled.div`
  background-color: ${props => props.theme.primaryDarkBrand};
  padding: ${unitcalc(2.5)} ${unitcalc(3)} ${unitcalc(2)} ${unitcalc(3)};
`;

const StyledLogo = styled(Img)`
  width: ${remcalc(87)};
  height: ${remcalc(25)};
`;

export default () => (
  <StyledHeader>
    <Row>
      <Col lg={12} xs={12}>
        <Link to="/">
          <StyledLogo src={Logo} />
        </Link>
      </Col>
    </Row>
  </StyledHeader>
);
<commit_msg>chore(cp-frontend): assert `nsc` imports are after `sc`'s<commit_after>import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Img } from 'normalized-styled-components';
import remcalc from 'remcalc';
import unitcalc from 'unitcalc';

import Logo from '@assets/triton_logo.png';
import { Col, Row } from 'react-styled-flexboxgrid';

const StyledHeader = styled.div`
  background-color: ${props => props.theme.primaryDarkBrand};
  padding: ${unitcalc(2.5)} ${unitcalc(3)} ${unitcalc(2)} ${unitcalc(3)};
`;

const StyledLogo = styled(Img)`
  width: ${remcalc(87)};
  height: ${remcalc(25)};
`;

export default () => (
  <StyledHeader>
    <Row>
      <Col lg={12} xs={12}>
        <Link to="/">
          <StyledLogo src={Logo} />
        </Link>
      </Col>
    </Row>
  </StyledHeader>
);
<|endoftext|>


<commit_before>// @flow

import {
  getEntityRanges,
  BLOCK_TYPE,
  ENTITY_TYPE,
  INLINE_STYLE,
} from 'draft-js-utils';

import type {ContentState, ContentBlock} from 'draft-js';

type Options = {
  gfm?: ?boolean;
};

const {BOLD, CODE, ITALIC, STRIKETHROUGH, UNDERLINE} = INLINE_STYLE;

const CODE_INDENT = '    ';

const defaultOptions: Options = {
  gfm: false,
};

class MarkupGenerator {
  blocks: Array<ContentBlock>;
  contentState: ContentState;
  currentBlock: number;
  output: Array<string>;
  totalBlocks: number;
  listItemCounts: Object;
  options: Options;

  constructor(contentState: ContentState, options?: Options) {
    this.contentState = contentState;
    this.options = options || defaultOptions;
  }

  generate(): string {
    this.output = [];
    this.blocks = this.contentState.getBlockMap().toArray();
    this.totalBlocks = this.blocks.length;
    this.currentBlock = 0;
    this.listItemCounts = {};
    while (this.currentBlock < this.totalBlocks) {
      this.processBlock();
    }
    return this.output.join('');
  }

  processBlock() {
    let block = this.blocks[this.currentBlock];
    let blockType = block.getType();
    switch (blockType) {
      case BLOCK_TYPE.HEADER_ONE: {
        this.insertLineBreaks(1);
        this.output.push('# ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_TWO: {
        this.insertLineBreaks(1);
        this.output.push('## ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_THREE: {
        this.insertLineBreaks(1);
        this.output.push('### ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_FOUR: {
        this.insertLineBreaks(1);
        this.output.push('#### ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_FIVE: {
        this.insertLineBreaks(1);
        this.output.push('##### ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_SIX: {
        this.insertLineBreaks(1);
        this.output.push('###### ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.UNORDERED_LIST_ITEM: {
        let blockDepth = block.getDepth();
        let lastBlock = this.getLastBlock();
        let lastBlockType = lastBlock ? lastBlock.getType() : null;
        let lastBlockDepth =
          lastBlock && canHaveDepth(lastBlockType)
            ? lastBlock.getDepth()
            : null;
        if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
          this.insertLineBreaks(1);
          // Insert an additional line break if following opposite list type.
          if (lastBlockType === BLOCK_TYPE.ORDERED_LIST_ITEM) {
            this.insertLineBreaks(1);
          }
        }
        let indent = ' '.repeat(block.depth * 4);
        this.output.push(indent + '- ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.ORDERED_LIST_ITEM: {
        let blockDepth = block.getDepth();
        let lastBlock = this.getLastBlock();
        let lastBlockType = lastBlock ? lastBlock.getType() : null;
        let lastBlockDepth =
          lastBlock && canHaveDepth(lastBlockType)
            ? lastBlock.getDepth()
            : null;
        if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
          this.insertLineBreaks(1);
          // Insert an additional line break if following opposite list type.
          if (lastBlockType === BLOCK_TYPE.UNORDERED_LIST_ITEM) {
            this.insertLineBreaks(1);
          }
        }
        let indent = ' '.repeat(blockDepth * 4);
        // TODO: figure out what to do with two-digit numbers
        let count = this.getListItemCount(block) % 10;
        this.output.push(
          indent + `${count}. ` + this.renderBlockContent(block) + '\n',
        );
        break;
      }
      case BLOCK_TYPE.BLOCKQUOTE: {
        this.insertLineBreaks(1);
        this.output.push(' > ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.CODE: {
        this.insertLineBreaks(1);
        if (this.options.gfm) {
          this.output.push('```\n');
          this.output.push(this.renderBlockContent(block) + '\n');
          this.output.push('```\n');
        } else {
          this.output.push(CODE_INDENT + this.renderBlockContent(block) + '\n');
        }
        break;
      }
      default: {
        this.insertLineBreaks(1);
        this.output.push(this.renderBlockContent(block) + '\n');
        break;
      }
    }
    this.currentBlock += 1;
  }

  getLastBlock(): ContentBlock {
    return this.blocks[this.currentBlock - 1];
  }

  getNextBlock(): ContentBlock {
    return this.blocks[this.currentBlock + 1];
  }

  getListItemCount(block: ContentBlock): number {
    let blockType = block.getType();
    let blockDepth = block.getDepth();
    // To decide if we need to start over we need to backtrack (skipping list
    // items that are of greater depth)
    let index = this.currentBlock - 1;
    let prevBlock = this.blocks[index];
    while (
      prevBlock &&
      canHaveDepth(prevBlock.getType()) &&
      prevBlock.getDepth() > blockDepth
    ) {
      index -= 1;
      prevBlock = this.blocks[index];
    }
    if (
      !prevBlock ||
      prevBlock.getType() !== blockType ||
      prevBlock.getDepth() !== blockDepth
    ) {
      this.listItemCounts[blockDepth] = 0;
    }
    return (this.listItemCounts[blockDepth] =
      this.listItemCounts[blockDepth] + 1);
  }

  insertLineBreaks(n: number) {
    if (this.currentBlock > 0) {
      for (let i = 0; i < n; i++) {
        this.output.push('\n');
      }
    }
  }

  renderBlockContent(block: ContentBlock): string {
    let {contentState} = this;
    let blockType = block.getType();
    let text = block.getText();
    if (text === '') {
      // Prevent element collapse if completely empty.
      // TODO: Replace with constant.
      return '\u200B';
    }
    let charMetaList = block.getCharacterList();
    let entityPieces = getEntityRanges(text, charMetaList);
    return entityPieces
      .map(([entityKey, stylePieces]) => {
        let content = stylePieces
          .map(([text, style]) => {
            // Don't allow empty inline elements.
            if (!text) {
              return '';
            }
            let content = text;
            // Don't encode any text inside a code block.
            if (blockType === BLOCK_TYPE.CODE) {
              return content;
            }
            // NOTE: We attempt some basic character escaping here, although
            // I don't know if escape sequences are really valid in markdown,
            // there's not a canonical spec to lean on.
            if (style.has(CODE)) {
              return '`' + encodeCode(content) + '`';
            }
            content = encodeContent(text);
            if (style.has(BOLD)) {
              content = `**${content}**`;
            }
            if (style.has(UNDERLINE)) {
              // TODO: encode `+`?
              content = `++${content}++`;
            }
            if (style.has(ITALIC)) {
              content = `_${content}_`;
            }
            if (style.has(STRIKETHROUGH)) {
              // TODO: encode `~`?
              content = `~~${content}~~`;
            }
            return content;
          })
          .join('');
        let entity = entityKey ? contentState.getEntity(entityKey) : null;
        if (entity != null && entity.getType() === ENTITY_TYPE.LINK) {
          let data = entity.getData();
          let url = data.url || '';
          let title = data.title ? ` "${escapeTitle(data.title)}"` : '';
          return `[${content}](${encodeURL(url)}${title})`;
        } else if (entity != null && entity.getType() === ENTITY_TYPE.IMAGE) {
          let data = entity.getData();
          let src = data.src || '';
          let alt = data.alt ? `${escapeTitle(data.alt)}` : '';
          return `![${alt}](${encodeURL(src)})`;
        } else {
          return content;
        }
      })
      .join('');
  }
}

function canHaveDepth(blockType: any): boolean {
  switch (blockType) {
    case BLOCK_TYPE.UNORDERED_LIST_ITEM:
    case BLOCK_TYPE.ORDERED_LIST_ITEM:
      return true;
    default:
      return false;
  }
}

function encodeContent(text) {
  return text.replace(/[*_`]/g, '\\$&');
}

function encodeCode(text) {
  return text.replace(/`/g, '\\`');
}

// Encode chars that would normally be allowed in a URL but would conflict with
// our markdown syntax: `[foo](http://foo/)`
function encodeURL(url) {
  return url.replace(/\)/g, '%29');
}

// Escape quotes using backslash.
function escapeTitle(text) {
  return text.replace(/"/g, '\\"');
}

export default function stateToMarkdown(
  content: ContentState,
  options?: Options,
): string {
  return new MarkupGenerator(content, options).generate();
}
<commit_msg>updated url->href key typo (#170)<commit_after>// @flow

import {
  getEntityRanges,
  BLOCK_TYPE,
  ENTITY_TYPE,
  INLINE_STYLE,
} from 'draft-js-utils';

import type {ContentState, ContentBlock} from 'draft-js';

type Options = {
  gfm?: ?boolean;
};

const {BOLD, CODE, ITALIC, STRIKETHROUGH, UNDERLINE} = INLINE_STYLE;

const CODE_INDENT = '    ';

const defaultOptions: Options = {
  gfm: false,
};

class MarkupGenerator {
  blocks: Array<ContentBlock>;
  contentState: ContentState;
  currentBlock: number;
  output: Array<string>;
  totalBlocks: number;
  listItemCounts: Object;
  options: Options;

  constructor(contentState: ContentState, options?: Options) {
    this.contentState = contentState;
    this.options = options || defaultOptions;
  }

  generate(): string {
    this.output = [];
    this.blocks = this.contentState.getBlockMap().toArray();
    this.totalBlocks = this.blocks.length;
    this.currentBlock = 0;
    this.listItemCounts = {};
    while (this.currentBlock < this.totalBlocks) {
      this.processBlock();
    }
    return this.output.join('');
  }

  processBlock() {
    let block = this.blocks[this.currentBlock];
    let blockType = block.getType();
    switch (blockType) {
      case BLOCK_TYPE.HEADER_ONE: {
        this.insertLineBreaks(1);
        this.output.push('# ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_TWO: {
        this.insertLineBreaks(1);
        this.output.push('## ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_THREE: {
        this.insertLineBreaks(1);
        this.output.push('### ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_FOUR: {
        this.insertLineBreaks(1);
        this.output.push('#### ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_FIVE: {
        this.insertLineBreaks(1);
        this.output.push('##### ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.HEADER_SIX: {
        this.insertLineBreaks(1);
        this.output.push('###### ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.UNORDERED_LIST_ITEM: {
        let blockDepth = block.getDepth();
        let lastBlock = this.getLastBlock();
        let lastBlockType = lastBlock ? lastBlock.getType() : null;
        let lastBlockDepth =
          lastBlock && canHaveDepth(lastBlockType)
            ? lastBlock.getDepth()
            : null;
        if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
          this.insertLineBreaks(1);
          // Insert an additional line break if following opposite list type.
          if (lastBlockType === BLOCK_TYPE.ORDERED_LIST_ITEM) {
            this.insertLineBreaks(1);
          }
        }
        let indent = ' '.repeat(block.depth * 4);
        this.output.push(indent + '- ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.ORDERED_LIST_ITEM: {
        let blockDepth = block.getDepth();
        let lastBlock = this.getLastBlock();
        let lastBlockType = lastBlock ? lastBlock.getType() : null;
        let lastBlockDepth =
          lastBlock && canHaveDepth(lastBlockType)
            ? lastBlock.getDepth()
            : null;
        if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
          this.insertLineBreaks(1);
          // Insert an additional line break if following opposite list type.
          if (lastBlockType === BLOCK_TYPE.UNORDERED_LIST_ITEM) {
            this.insertLineBreaks(1);
          }
        }
        let indent = ' '.repeat(blockDepth * 4);
        // TODO: figure out what to do with two-digit numbers
        let count = this.getListItemCount(block) % 10;
        this.output.push(
          indent + `${count}. ` + this.renderBlockContent(block) + '\n',
        );
        break;
      }
      case BLOCK_TYPE.BLOCKQUOTE: {
        this.insertLineBreaks(1);
        this.output.push(' > ' + this.renderBlockContent(block) + '\n');
        break;
      }
      case BLOCK_TYPE.CODE: {
        this.insertLineBreaks(1);
        if (this.options.gfm) {
          this.output.push('```\n');
          this.output.push(this.renderBlockContent(block) + '\n');
          this.output.push('```\n');
        } else {
          this.output.push(CODE_INDENT + this.renderBlockContent(block) + '\n');
        }
        break;
      }
      default: {
        this.insertLineBreaks(1);
        this.output.push(this.renderBlockContent(block) + '\n');
        break;
      }
    }
    this.currentBlock += 1;
  }

  getLastBlock(): ContentBlock {
    return this.blocks[this.currentBlock - 1];
  }

  getNextBlock(): ContentBlock {
    return this.blocks[this.currentBlock + 1];
  }

  getListItemCount(block: ContentBlock): number {
    let blockType = block.getType();
    let blockDepth = block.getDepth();
    // To decide if we need to start over we need to backtrack (skipping list
    // items that are of greater depth)
    let index = this.currentBlock - 1;
    let prevBlock = this.blocks[index];
    while (
      prevBlock &&
      canHaveDepth(prevBlock.getType()) &&
      prevBlock.getDepth() > blockDepth
    ) {
      index -= 1;
      prevBlock = this.blocks[index];
    }
    if (
      !prevBlock ||
      prevBlock.getType() !== blockType ||
      prevBlock.getDepth() !== blockDepth
    ) {
      this.listItemCounts[blockDepth] = 0;
    }
    return (this.listItemCounts[blockDepth] =
      this.listItemCounts[blockDepth] + 1);
  }

  insertLineBreaks(n: number) {
    if (this.currentBlock > 0) {
      for (let i = 0; i < n; i++) {
        this.output.push('\n');
      }
    }
  }

  renderBlockContent(block: ContentBlock): string {
    let {contentState} = this;
    let blockType = block.getType();
    let text = block.getText();
    if (text === '') {
      // Prevent element collapse if completely empty.
      // TODO: Replace with constant.
      return '\u200B';
    }
    let charMetaList = block.getCharacterList();
    let entityPieces = getEntityRanges(text, charMetaList);
    return entityPieces
      .map(([entityKey, stylePieces]) => {
        let content = stylePieces
          .map(([text, style]) => {
            // Don't allow empty inline elements.
            if (!text) {
              return '';
            }
            let content = text;
            // Don't encode any text inside a code block.
            if (blockType === BLOCK_TYPE.CODE) {
              return content;
            }
            // NOTE: We attempt some basic character escaping here, although
            // I don't know if escape sequences are really valid in markdown,
            // there's not a canonical spec to lean on.
            if (style.has(CODE)) {
              return '`' + encodeCode(content) + '`';
            }
            content = encodeContent(text);
            if (style.has(BOLD)) {
              content = `**${content}**`;
            }
            if (style.has(UNDERLINE)) {
              // TODO: encode `+`?
              content = `++${content}++`;
            }
            if (style.has(ITALIC)) {
              content = `_${content}_`;
            }
            if (style.has(STRIKETHROUGH)) {
              // TODO: encode `~`?
              content = `~~${content}~~`;
            }
            return content;
          })
          .join('');
        let entity = entityKey ? contentState.getEntity(entityKey) : null;
        if (entity != null && entity.getType() === ENTITY_TYPE.LINK) {
          let data = entity.getData();
          let url = data.href || data.url || '';
          let title = data.title ? ` "${escapeTitle(data.title)}"` : '';
          return `[${content}](${encodeURL(url)}${title})`;
        } else if (entity != null && entity.getType() === ENTITY_TYPE.IMAGE) {
          let data = entity.getData();
          let src = data.src || '';
          let alt = data.alt ? `${escapeTitle(data.alt)}` : '';
          return `![${alt}](${encodeURL(src)})`;
        } else {
          return content;
        }
      })
      .join('');
  }
}

function canHaveDepth(blockType: any): boolean {
  switch (blockType) {
    case BLOCK_TYPE.UNORDERED_LIST_ITEM:
    case BLOCK_TYPE.ORDERED_LIST_ITEM:
      return true;
    default:
      return false;
  }
}

function encodeContent(text) {
  return text.replace(/[*_`]/g, '\\$&');
}

function encodeCode(text) {
  return text.replace(/`/g, '\\`');
}

// Encode chars that would normally be allowed in a URL but would conflict with
// our markdown syntax: `[foo](http://foo/)`
function encodeURL(url) {
  return url.replace(/\)/g, '%29');
}

// Escape quotes using backslash.
function escapeTitle(text) {
  return text.replace(/"/g, '\\"');
}

export default function stateToMarkdown(
  content: ContentState,
  options?: Options,
): string {
  return new MarkupGenerator(content, options).generate();
}
<|endoftext|>


<commit_before>// @flow

import replaceTextWithMeta from './lib/replaceTextWithMeta';
import {CharacterMetadata, ContentBlock, ContentState, genKey} from 'draft-js';
import {List, Map, OrderedSet, Repeat, Seq} from 'immutable';
import {BLOCK_TYPE, ENTITY_TYPE, INLINE_STYLE} from 'draft-js-utils';
import {NODE_TYPE_ELEMENT, NODE_TYPE_TEXT} from 'synthetic-dom';
import {
  INLINE_ELEMENTS,
  SPECIAL_ELEMENTS,
  SELF_CLOSING_ELEMENTS,
} from './lib/Constants';

import type {Entity} from 'draft-js';
import type {Set, IndexedSeq} from 'immutable';
import type {
  Node as SyntheticNode,
  ElementNode as SyntheticElement,
} from 'synthetic-dom';

type DOMNode = SyntheticNode | Node;
type DOMElement = SyntheticElement | Element;

type CharacterMetaSeq = IndexedSeq<CharacterMetadata>;
type Style = string;
type StyleSet = Set<Style>;

type TextFragment = {
  text: string;
  characterMeta: CharacterMetaSeq;
};

type BlockData = {[key: string]: mixed};

// A ParsedBlock has two purposes:
//   1) to keep data about the block (textFragments, type)
//   2) to act as some context for storing parser state as we parse its contents
type ParsedBlock = {
  tagName: string;
  textFragments: Array<TextFragment>;
  type: string;
  // A stack in which the last item represents the styles that will apply
  // to any text node descendants.
  styleStack: Array<StyleSet>;
  entityStack: Array<?Entity>;
  depth: number;
  data: ?BlockData;
};

export type ElementStyles = {[tagName: string]: Style};

export type CustomBlockFn = (
  element: DOMElement,
) => ?{type?: string; data?: BlockData};

type Options = {
  elementStyles?: ElementStyles;
  blockTypes?: {[key: string]: string};
  customBlockFn?: CustomBlockFn;
};

const NO_STYLE = OrderedSet();
const NO_ENTITY = null;

const EMPTY_BLOCK = new ContentBlock({
  key: genKey(),
  text: '',
  type: BLOCK_TYPE.UNSTYLED,
  characterList: List(),
  depth: 0,
});

const LINE_BREAKS = /(\r\n|\r|\n)/g;
// We use `\r` because that character is always stripped from source (normalized
// to `\n`), so it's safe to assume it will only appear in the text content when
// we put it there as a placeholder.
const SOFT_BREAK_PLACEHOLDER = '\r';
const ZERO_WIDTH_SPACE = '\u200B';
const DATA_ATTRIBUTE = /^data-([a-z0-9-]+)$/;

// Map element attributes to entity data.
const ELEM_ATTR_MAP = {
  a: {href: 'url', rel: 'rel', target: 'target', title: 'title'},
  img: {src: 'src', alt: 'alt'},
};

const getEntityData = (tagName: string, element: DOMElement) => {
  const data = {};
  if (ELEM_ATTR_MAP.hasOwnProperty(tagName)) {
    const attrMap = ELEM_ATTR_MAP[tagName];
    for (let i = 0; i < element.attributes.length; i++) {
      const {name, value} = element.attributes[i];
      if (value != null) {
        if (attrMap.hasOwnProperty(name)) {
          const newName = attrMap[name];
          data[newName] = value;
        } else if (DATA_ATTRIBUTE.test(name)) {
          data[name] = value;
        }
      }
    }
  }
  return data;
};

// Functions to convert elements to entities.
const ElementToEntity = {
  a(
    generator: ContentGenerator,
    tagName: string,
    element: DOMElement,
  ): ?string {
    let data = getEntityData(tagName, element);
    // Don't add `<a>` elements with no href.
    if (data.url != null) {
      return generator.createEntity(ENTITY_TYPE.LINK, 'MUTABLE', data);
    }
  },
  img(
    generator: ContentGenerator,
    tagName: string,
    element: DOMElement,
  ): ?string {
    let data = getEntityData(tagName, element);
    // Don't add `<img>` elements with no src.
    if (data.src != null) {
      return generator.createEntity(ENTITY_TYPE.IMAGE, 'MUTABLE', data);
    }
  },
};

class ContentGenerator {
  contentStateForEntities: ContentState;
  blockStack: Array<ParsedBlock>;
  blockList: Array<ParsedBlock>;
  depth: number;
  options: Options;

  constructor(options: Options = {}) {
    this.options = options;
    this.contentStateForEntities = ContentState.createFromBlockArray([]);
    // This represents the hierarchy as we traverse nested elements; for
    // example [body, ul, li] where we must know li's parent type (ul or ol).
    this.blockStack = [];
    // This is a linear list of blocks that will form the output; for example
    // [p, li, li, blockquote].
    this.blockList = [];
    this.depth = 0;
  }

  process(element: DOMElement): ContentState {
    this.processBlockElement(element);
    let contentBlocks = [];
    this.blockList.forEach((block) => {
      let {text, characterMeta} = concatFragments(block.textFragments);
      let includeEmptyBlock = false;
      // If the block contains only a soft break then don't discard the block,
      // but discard the soft break.
      if (text === SOFT_BREAK_PLACEHOLDER) {
        includeEmptyBlock = true;
        text = '';
      }
      if (block.tagName === 'pre') {
        ({text, characterMeta} = trimLeadingNewline(text, characterMeta));
      } else {
        ({text, characterMeta} = collapseWhiteSpace(text, characterMeta));
      }
      // Previously we were using a placeholder for soft breaks. Now that we
      // have collapsed whitespace we can change it back to normal line breaks.
      text = text.split(SOFT_BREAK_PLACEHOLDER).join('\n');
      // Discard empty blocks (unless otherwise specified).
      if (text.length || includeEmptyBlock) {
        contentBlocks.push(
          new ContentBlock({
            key: genKey(),
            text: text,
            type: block.type,
            characterList: characterMeta.toList(),
            depth: block.depth,
            data: block.data ? Map(block.data) : Map(),
          }),
        );
      }
    });
    if (!contentBlocks.length) {
      contentBlocks = [EMPTY_BLOCK];
    }
    return ContentState.createFromBlockArray(
      contentBlocks,
      this.contentStateForEntities.getEntityMap(),
    );
  }

  getBlockTypeFromTagName(tagName: string): string {
    let {blockTypes} = this.options;
    if (blockTypes && blockTypes[tagName]) {
      return blockTypes[tagName];
    }
    switch (tagName) {
      case 'li': {
        let parent = this.blockStack.slice(-1)[0];
        return parent.tagName === 'ol'
          ? BLOCK_TYPE.ORDERED_LIST_ITEM
          : BLOCK_TYPE.UNORDERED_LIST_ITEM;
      }
      case 'blockquote': {
        return BLOCK_TYPE.BLOCKQUOTE;
      }
      case 'h1': {
        return BLOCK_TYPE.HEADER_ONE;
      }
      case 'h2': {
        return BLOCK_TYPE.HEADER_TWO;
      }
      case 'h3': {
        return BLOCK_TYPE.HEADER_THREE;
      }
      case 'h4': {
        return BLOCK_TYPE.HEADER_FOUR;
      }
      case 'h5': {
        return BLOCK_TYPE.HEADER_FIVE;
      }
      case 'h6': {
        return BLOCK_TYPE.HEADER_SIX;
      }
      case 'pre': {
        return BLOCK_TYPE.CODE;
      }
      case 'figure': {
        return BLOCK_TYPE.ATOMIC;
      }
      default: {
        return BLOCK_TYPE.UNSTYLED;
      }
    }
  }

  processBlockElement(element: DOMElement) {
    if (!element) {
      return;
    }
    let {customBlockFn} = this.options;
    let tagName = element.nodeName.toLowerCase();
    let type: ?string;
    let data: ?BlockData;
    if (customBlockFn) {
      let customBlock = customBlockFn(element);
      if (customBlock != null) {
        type = customBlock.type;
        data = customBlock.data;
      }
    }
    if (type == null) {
      type = this.getBlockTypeFromTagName(tagName);
    }
    let hasDepth = canHaveDepth(type);
    let allowRender = !SPECIAL_ELEMENTS.hasOwnProperty(tagName);
    let block: ParsedBlock = {
      tagName: tagName,
      textFragments: [],
      type: type,
      styleStack: [NO_STYLE],
      entityStack: [NO_ENTITY],
      depth: hasDepth ? this.depth : 0,
      data,
    };
    if (allowRender) {
      this.blockList.push(block);
      if (hasDepth) {
        this.depth += 1;
      }
    }
    this.blockStack.push(block);
    if (element.childNodes != null) {
      Array.from(element.childNodes).forEach(this.processNode, this);
    }
    this.blockStack.pop();
    if (allowRender && hasDepth) {
      this.depth -= 1;
    }
  }

  processInlineElement(element: DOMElement) {
    let tagName = element.nodeName.toLowerCase();
    if (tagName === 'br') {
      this.processText(SOFT_BREAK_PLACEHOLDER);
      return;
    }
    let block = this.blockStack.slice(-1)[0];
    let style = block.styleStack.slice(-1)[0];
    let entityKey = block.entityStack.slice(-1)[0];
    style = addStyleFromTagName(style, tagName, this.options.elementStyles);
    if (ElementToEntity.hasOwnProperty(tagName)) {
      // If the to-entity function returns nothing, use the existing entity.
      entityKey = ElementToEntity[tagName](this, tagName, element) || entityKey;
    }
    block.styleStack.push(style);
    block.entityStack.push(entityKey);
    if (element.childNodes != null) {
      Array.from(element.childNodes).forEach(this.processNode, this);
    }
    if (SELF_CLOSING_ELEMENTS.hasOwnProperty(tagName)) {
      this.processText('\u00A0');
    }
    block.entityStack.pop();
    block.styleStack.pop();
  }

  processTextNode(node: DOMNode) {
    let text = node.nodeValue;
    // This is important because we will use \r as a placeholder for a soft break.
    text = text.replace(LINE_BREAKS, '\n');
    // Replace zero-width space (we use it as a placeholder in markdown) with a
    // soft break.
    // TODO: The import-markdown package should correctly turn breaks into <br>
    // elements so we don't need to include this hack.
    text = text.split(ZERO_WIDTH_SPACE).join(SOFT_BREAK_PLACEHOLDER);
    this.processText(text);
  }

  processText(text: string) {
    let block = this.blockStack.slice(-1)[0];
    let style = block.styleStack.slice(-1)[0];
    let entity = block.entityStack.slice(-1)[0];
    let charMetadata = CharacterMetadata.create({
      style: style,
      entity: entity,
    });
    let seq: CharacterMetaSeq = Repeat(charMetadata, text.length);
    block.textFragments.push({
      text: text,
      characterMeta: seq,
    });
  }

  processNode(node: DOMNode) {
    if (node.nodeType === NODE_TYPE_ELEMENT) {
      // $FlowIssue
      let element: DOMElement = node;
      let tagName = element.nodeName.toLowerCase();
      if (INLINE_ELEMENTS.hasOwnProperty(tagName)) {
        this.processInlineElement(element);
      } else {
        this.processBlockElement(element);
      }
    } else if (node.nodeType === NODE_TYPE_TEXT) {
      this.processTextNode(node);
    }
  }

  createEntity(type: string, mutability: string, data: Object) {
    this.contentStateForEntities = this.contentStateForEntities.createEntity(
      type,
      mutability,
      data,
    );
    return this.contentStateForEntities.getLastCreatedEntityKey();
  }
}

function trimLeadingNewline(
  text: string,
  characterMeta: CharacterMetaSeq,
): TextFragment {
  if (text.charAt(0) === '\n') {
    text = text.slice(1);
    characterMeta = characterMeta.slice(1);
  }
  return {text, characterMeta};
}

function trimLeadingSpace(
  text: string,
  characterMeta: CharacterMetaSeq,
): TextFragment {
  while (text.charAt(0) === ' ') {
    text = text.slice(1);
    characterMeta = characterMeta.slice(1);
  }
  return {text, characterMeta};
}

function trimTrailingSpace(
  text: string,
  characterMeta: CharacterMetaSeq,
): TextFragment {
  while (text.slice(-1) === ' ') {
    text = text.slice(0, -1);
    characterMeta = characterMeta.slice(0, -1);
  }
  return {text, characterMeta};
}

function collapseWhiteSpace(
  text: string,
  characterMeta: CharacterMetaSeq,
): TextFragment {
  text = text.replace(/[ \t\n]/g, ' ');
  ({text, characterMeta} = trimLeadingSpace(text, characterMeta));
  ({text, characterMeta} = trimTrailingSpace(text, characterMeta));
  let i = text.length;
  while (i--) {
    if (text.charAt(i) === ' ' && text.charAt(i - 1) === ' ') {
      text = text.slice(0, i) + text.slice(i + 1);
      characterMeta = characterMeta
        .slice(0, i)
        .concat(characterMeta.slice(i + 1));
    }
  }
  // There could still be one space on either side of a softbreak.
  ({text, characterMeta} = replaceTextWithMeta(
    {text, characterMeta},
    SOFT_BREAK_PLACEHOLDER + ' ',
    SOFT_BREAK_PLACEHOLDER,
  ));
  ({text, characterMeta} = replaceTextWithMeta(
    {text, characterMeta},
    ' ' + SOFT_BREAK_PLACEHOLDER,
    SOFT_BREAK_PLACEHOLDER,
  ));
  return {text, characterMeta};
}

function canHaveDepth(blockType: string): boolean {
  switch (blockType) {
    case BLOCK_TYPE.UNORDERED_LIST_ITEM:
    case BLOCK_TYPE.ORDERED_LIST_ITEM: {
      return true;
    }
    default: {
      return false;
    }
  }
}

function concatFragments(fragments: Array<TextFragment>): TextFragment {
  let text = '';
  let characterMeta: CharacterMetaSeq = Seq();
  fragments.forEach((textFragment: TextFragment) => {
    text = text + textFragment.text;
    characterMeta = characterMeta.concat(textFragment.characterMeta);
  });
  return {text, characterMeta};
}

function addStyleFromTagName(
  styleSet: StyleSet,
  tagName: string,
  elementStyles?: ElementStyles,
): StyleSet {
  switch (tagName) {
    case 'b':
    case 'strong': {
      return styleSet.add(INLINE_STYLE.BOLD);
    }
    case 'i':
    case 'em': {
      return styleSet.add(INLINE_STYLE.ITALIC);
    }
    case 'ins': {
      return styleSet.add(INLINE_STYLE.UNDERLINE);
    }
    case 'code': {
      return styleSet.add(INLINE_STYLE.CODE);
    }
    case 'del': {
      return styleSet.add(INLINE_STYLE.STRIKETHROUGH);
    }
    default: {
      // Allow custom styles to be provided.
      if (elementStyles && elementStyles[tagName]) {
        return styleSet.add(elementStyles[tagName]);
      }

      return styleSet;
    }
  }
}

export function stateFromElement(
  element: DOMElement,
  options?: Options,
): ContentState {
  return new ContentGenerator(options).process(element);
}

export default stateFromElement;
<commit_msg>import-element: Implement hasSemanticMeaning<commit_after>// @flow

import replaceTextWithMeta from './lib/replaceTextWithMeta';
import {CharacterMetadata, ContentBlock, ContentState, genKey} from 'draft-js';
import {List, Map, OrderedSet, Repeat, Seq} from 'immutable';
import {BLOCK_TYPE, ENTITY_TYPE, INLINE_STYLE} from 'draft-js-utils';
import {NODE_TYPE_ELEMENT, NODE_TYPE_TEXT} from 'synthetic-dom';
import {
  INLINE_ELEMENTS,
  SPECIAL_ELEMENTS,
  SELF_CLOSING_ELEMENTS,
} from './lib/Constants';

import type {Entity} from 'draft-js';
import type {Set, IndexedSeq} from 'immutable';
import type {
  Node as SyntheticNode,
  ElementNode as SyntheticElement,
} from 'synthetic-dom';

type DOMNode = SyntheticNode | Node;
type DOMElement = SyntheticElement | Element;

type CharacterMetaSeq = IndexedSeq<CharacterMetadata>;
type Style = string;
type StyleSet = Set<Style>;

type TextFragment = {
  text: string;
  characterMeta: CharacterMetaSeq;
};

type BlockData = {[key: string]: mixed};

// A ParsedBlock has two purposes:
//   1) to keep data about the block (textFragments, type)
//   2) to act as some context for storing parser state as we parse its contents
type ParsedBlock = {
  tagName: string;
  textFragments: Array<TextFragment>;
  type: string;
  // A stack in which the last item represents the styles that will apply
  // to any text node descendants.
  styleStack: Array<StyleSet>;
  entityStack: Array<?Entity>;
  depth: number;
  data: ?BlockData;
};

export type ElementStyles = {[tagName: string]: Style};

export type CustomBlockFn = (
  element: DOMElement,
) => ?{type?: string; data?: BlockData};

type Options = {
  elementStyles?: ElementStyles;
  blockTypes?: {[key: string]: string};
  customBlockFn?: CustomBlockFn;
};

const NO_STYLE = OrderedSet();
const NO_ENTITY = null;

const EMPTY_BLOCK = new ContentBlock({
  key: genKey(),
  text: '',
  type: BLOCK_TYPE.UNSTYLED,
  characterList: List(),
  depth: 0,
});

const LINE_BREAKS = /(\r\n|\r|\n)/g;
// We use `\r` because that character is always stripped from source (normalized
// to `\n`), so it's safe to assume it will only appear in the text content when
// we put it there as a placeholder.
const SOFT_BREAK_PLACEHOLDER = '\r';
const ZERO_WIDTH_SPACE = '\u200B';
const DATA_ATTRIBUTE = /^data-([a-z0-9-]+)$/;

// Map element attributes to entity data.
const ELEM_ATTR_MAP = {
  a: {href: 'url', rel: 'rel', target: 'target', title: 'title'},
  img: {src: 'src', alt: 'alt'},
};

const getEntityData = (tagName: string, element: DOMElement) => {
  const data = {};
  if (ELEM_ATTR_MAP.hasOwnProperty(tagName)) {
    const attrMap = ELEM_ATTR_MAP[tagName];
    for (let i = 0; i < element.attributes.length; i++) {
      const {name, value} = element.attributes[i];
      if (value != null) {
        if (attrMap.hasOwnProperty(name)) {
          const newName = attrMap[name];
          data[newName] = value;
        } else if (DATA_ATTRIBUTE.test(name)) {
          data[name] = value;
        }
      }
    }
  }
  return data;
};

// Functions to convert elements to entities.
const ElementToEntity = {
  a(
    generator: ContentGenerator,
    tagName: string,
    element: DOMElement,
  ): ?string {
    let data = getEntityData(tagName, element);
    // Don't add `<a>` elements with no href.
    if (data.url != null) {
      return generator.createEntity(ENTITY_TYPE.LINK, 'MUTABLE', data);
    }
  },
  img(
    generator: ContentGenerator,
    tagName: string,
    element: DOMElement,
  ): ?string {
    let data = getEntityData(tagName, element);
    // Don't add `<img>` elements with no src.
    if (data.src != null) {
      return generator.createEntity(ENTITY_TYPE.IMAGE, 'MUTABLE', data);
    }
  },
};

class ContentGenerator {
  contentStateForEntities: ContentState;
  blockStack: Array<ParsedBlock>;
  blockList: Array<ParsedBlock>;
  depth: number;
  options: Options;

  constructor(options: Options = {}) {
    this.options = options;
    this.contentStateForEntities = ContentState.createFromBlockArray([]);
    // This represents the hierarchy as we traverse nested elements; for
    // example [body, ul, li] where we must know li's parent type (ul or ol).
    this.blockStack = [];
    // This is a linear list of blocks that will form the output; for example
    // [p, li, li, blockquote].
    this.blockList = [];
    this.depth = 0;
  }

  process(element: DOMElement): ContentState {
    this.processBlockElement(element);
    let contentBlocks = [];
    this.blockList.forEach((block) => {
      let {text, characterMeta} = concatFragments(block.textFragments);
      let includeEmptyBlock = false;
      // If the block contains only a soft break then don't discard the block,
      // but discard the soft break.
      if (text === SOFT_BREAK_PLACEHOLDER) {
        includeEmptyBlock = true;
        text = '';
      }
      if (block.tagName === 'pre') {
        ({text, characterMeta} = trimLeadingNewline(text, characterMeta));
      } else {
        ({text, characterMeta} = collapseWhiteSpace(text, characterMeta));
      }
      // Previously we were using a placeholder for soft breaks. Now that we
      // have collapsed whitespace we can change it back to normal line breaks.
      text = text.split(SOFT_BREAK_PLACEHOLDER).join('\n');
      // Discard empty blocks (unless otherwise specified).
      if (text.length || includeEmptyBlock) {
        contentBlocks.push(
          new ContentBlock({
            key: genKey(),
            text: text,
            type: block.type,
            characterList: characterMeta.toList(),
            depth: block.depth,
            data: block.data ? Map(block.data) : Map(),
          }),
        );
      }
    });
    if (!contentBlocks.length) {
      contentBlocks = [EMPTY_BLOCK];
    }
    return ContentState.createFromBlockArray(
      contentBlocks,
      this.contentStateForEntities.getEntityMap(),
    );
  }

  getBlockTypeFromTagName(tagName: string): string {
    let {blockTypes} = this.options;
    if (blockTypes && blockTypes[tagName]) {
      return blockTypes[tagName];
    }
    switch (tagName) {
      case 'li': {
        let parent = this.blockStack.slice(-1)[0];
        return parent.tagName === 'ol'
          ? BLOCK_TYPE.ORDERED_LIST_ITEM
          : BLOCK_TYPE.UNORDERED_LIST_ITEM;
      }
      case 'blockquote': {
        return BLOCK_TYPE.BLOCKQUOTE;
      }
      case 'h1': {
        return BLOCK_TYPE.HEADER_ONE;
      }
      case 'h2': {
        return BLOCK_TYPE.HEADER_TWO;
      }
      case 'h3': {
        return BLOCK_TYPE.HEADER_THREE;
      }
      case 'h4': {
        return BLOCK_TYPE.HEADER_FOUR;
      }
      case 'h5': {
        return BLOCK_TYPE.HEADER_FIVE;
      }
      case 'h6': {
        return BLOCK_TYPE.HEADER_SIX;
      }
      case 'pre': {
        return BLOCK_TYPE.CODE;
      }
      case 'figure': {
        return BLOCK_TYPE.ATOMIC;
      }
      default: {
        return BLOCK_TYPE.UNSTYLED;
      }
    }
  }

  processBlockElement(element: DOMElement) {
    if (!element) {
      return;
    }
    let {customBlockFn} = this.options;
    let tagName = element.nodeName.toLowerCase();
    let type: ?string;
    let data: ?BlockData;
    if (customBlockFn) {
      let customBlock = customBlockFn(element);
      if (customBlock != null) {
        type = customBlock.type;
        data = customBlock.data;
      }
    }
    let isCustomType = true;
    if (type == null) {
      isCustomType = false;
      type = this.getBlockTypeFromTagName(tagName);
    }
    let hasDepth = canHaveDepth(type);
    let allowRender = !SPECIAL_ELEMENTS.hasOwnProperty(tagName);
    if (!isCustomType && !hasSemanticMeaning(type)) {
      let parent = this.blockStack.slice(-1)[0];
      if (parent) {
        type = parent.type;
      }
    }
    let block: ParsedBlock = {
      tagName: tagName,
      textFragments: [],
      type: type,
      styleStack: [NO_STYLE],
      entityStack: [NO_ENTITY],
      depth: hasDepth ? this.depth : 0,
      data,
    };
    if (allowRender) {
      this.blockList.push(block);
      if (hasDepth) {
        this.depth += 1;
      }
    }
    this.blockStack.push(block);
    if (element.childNodes != null) {
      Array.from(element.childNodes).forEach(this.processNode, this);
    }
    this.blockStack.pop();
    if (allowRender && hasDepth) {
      this.depth -= 1;
    }
  }

  processInlineElement(element: DOMElement) {
    let tagName = element.nodeName.toLowerCase();
    if (tagName === 'br') {
      this.processText(SOFT_BREAK_PLACEHOLDER);
      return;
    }
    let block = this.blockStack.slice(-1)[0];
    let style = block.styleStack.slice(-1)[0];
    let entityKey = block.entityStack.slice(-1)[0];
    style = addStyleFromTagName(style, tagName, this.options.elementStyles);
    if (ElementToEntity.hasOwnProperty(tagName)) {
      // If the to-entity function returns nothing, use the existing entity.
      entityKey = ElementToEntity[tagName](this, tagName, element) || entityKey;
    }
    block.styleStack.push(style);
    block.entityStack.push(entityKey);
    if (element.childNodes != null) {
      Array.from(element.childNodes).forEach(this.processNode, this);
    }
    if (SELF_CLOSING_ELEMENTS.hasOwnProperty(tagName)) {
      this.processText('\u00A0');
    }
    block.entityStack.pop();
    block.styleStack.pop();
  }

  processTextNode(node: DOMNode) {
    let text = node.nodeValue;
    // This is important because we will use \r as a placeholder for a soft break.
    text = text.replace(LINE_BREAKS, '\n');
    // Replace zero-width space (we use it as a placeholder in markdown) with a
    // soft break.
    // TODO: The import-markdown package should correctly turn breaks into <br>
    // elements so we don't need to include this hack.
    text = text.split(ZERO_WIDTH_SPACE).join(SOFT_BREAK_PLACEHOLDER);
    this.processText(text);
  }

  processText(text: string) {
    let block = this.blockStack.slice(-1)[0];
    let style = block.styleStack.slice(-1)[0];
    let entity = block.entityStack.slice(-1)[0];
    let charMetadata = CharacterMetadata.create({
      style: style,
      entity: entity,
    });
    let seq: CharacterMetaSeq = Repeat(charMetadata, text.length);
    block.textFragments.push({
      text: text,
      characterMeta: seq,
    });
  }

  processNode(node: DOMNode) {
    if (node.nodeType === NODE_TYPE_ELEMENT) {
      // $FlowIssue
      let element: DOMElement = node;
      let tagName = element.nodeName.toLowerCase();
      if (INLINE_ELEMENTS.hasOwnProperty(tagName)) {
        this.processInlineElement(element);
      } else {
        this.processBlockElement(element);
      }
    } else if (node.nodeType === NODE_TYPE_TEXT) {
      this.processTextNode(node);
    }
  }

  createEntity(type: string, mutability: string, data: Object) {
    this.contentStateForEntities = this.contentStateForEntities.createEntity(
      type,
      mutability,
      data,
    );
    return this.contentStateForEntities.getLastCreatedEntityKey();
  }
}

function trimLeadingNewline(
  text: string,
  characterMeta: CharacterMetaSeq,
): TextFragment {
  if (text.charAt(0) === '\n') {
    text = text.slice(1);
    characterMeta = characterMeta.slice(1);
  }
  return {text, characterMeta};
}

function trimLeadingSpace(
  text: string,
  characterMeta: CharacterMetaSeq,
): TextFragment {
  while (text.charAt(0) === ' ') {
    text = text.slice(1);
    characterMeta = characterMeta.slice(1);
  }
  return {text, characterMeta};
}

function trimTrailingSpace(
  text: string,
  characterMeta: CharacterMetaSeq,
): TextFragment {
  while (text.slice(-1) === ' ') {
    text = text.slice(0, -1);
    characterMeta = characterMeta.slice(0, -1);
  }
  return {text, characterMeta};
}

function collapseWhiteSpace(
  text: string,
  characterMeta: CharacterMetaSeq,
): TextFragment {
  text = text.replace(/[ \t\n]/g, ' ');
  ({text, characterMeta} = trimLeadingSpace(text, characterMeta));
  ({text, characterMeta} = trimTrailingSpace(text, characterMeta));
  let i = text.length;
  while (i--) {
    if (text.charAt(i) === ' ' && text.charAt(i - 1) === ' ') {
      text = text.slice(0, i) + text.slice(i + 1);
      characterMeta = characterMeta
        .slice(0, i)
        .concat(characterMeta.slice(i + 1));
    }
  }
  // There could still be one space on either side of a softbreak.
  ({text, characterMeta} = replaceTextWithMeta(
    {text, characterMeta},
    SOFT_BREAK_PLACEHOLDER + ' ',
    SOFT_BREAK_PLACEHOLDER,
  ));
  ({text, characterMeta} = replaceTextWithMeta(
    {text, characterMeta},
    ' ' + SOFT_BREAK_PLACEHOLDER,
    SOFT_BREAK_PLACEHOLDER,
  ));
  return {text, characterMeta};
}

function canHaveDepth(blockType: string): boolean {
  switch (blockType) {
    case BLOCK_TYPE.UNORDERED_LIST_ITEM:
    case BLOCK_TYPE.ORDERED_LIST_ITEM: {
      return true;
    }
    default: {
      return false;
    }
  }
}

function concatFragments(fragments: Array<TextFragment>): TextFragment {
  let text = '';
  let characterMeta: CharacterMetaSeq = Seq();
  fragments.forEach((textFragment: TextFragment) => {
    text = text + textFragment.text;
    characterMeta = characterMeta.concat(textFragment.characterMeta);
  });
  return {text, characterMeta};
}

function addStyleFromTagName(
  styleSet: StyleSet,
  tagName: string,
  elementStyles?: ElementStyles,
): StyleSet {
  switch (tagName) {
    case 'b':
    case 'strong': {
      return styleSet.add(INLINE_STYLE.BOLD);
    }
    case 'i':
    case 'em': {
      return styleSet.add(INLINE_STYLE.ITALIC);
    }
    case 'ins': {
      return styleSet.add(INLINE_STYLE.UNDERLINE);
    }
    case 'code': {
      return styleSet.add(INLINE_STYLE.CODE);
    }
    case 'del': {
      return styleSet.add(INLINE_STYLE.STRIKETHROUGH);
    }
    default: {
      // Allow custom styles to be provided.
      if (elementStyles && elementStyles[tagName]) {
        return styleSet.add(elementStyles[tagName]);
      }

      return styleSet;
    }
  }
}

function hasSemanticMeaning(blockType: string) {
  return blockType !== BLOCK_TYPE.UNSTYLED;
}

export function stateFromElement(
  element: DOMElement,
  options?: Options,
): ContentState {
  return new ContentGenerator(options).process(element);
}

export default stateFromElement;
<|endoftext|>


<commit_before>/*global define*/
define([
        '../Core/defined',
        '../Core/defaultValue',
        '../Core/DeveloperError',
        '../Core/destroyObject',
        '../Core/Enumeration',
        '../Core/loadArrayBuffer',
        '../Core/loadText',
        '../Core/loadImage',
        '../Core/Queue',
        '../Core/IndexDatatype',
        '../Renderer/BufferUsage',
        './SceneMode'
    ], function(
        defined,
        defaultValue,
        DeveloperError,
        destroyObject,
        Enumeration,
        loadArrayBuffer,
        loadText,
        loadImage,
        Queue,
        IndexDatatype,
        BufferUsage,
        SceneMode) {
    "use strict";
// TODO: remove before merge to master
/*global console*/

    var ModelState = {
        NEEDS_LOAD : new Enumeration(0, 'NEEDS_LOAD'),
        LOADING : new Enumeration(1, 'LOADING'),
        LOADED : new Enumeration(2, 'LOADED')
    };

    function LoadResources() {
        this.bufferViewsToCreate = new Queue();
        this.buffers = {};
        this.pendingBufferLoads = 0;

        this.programsToCreate = new Queue();
        this.shaders = {};
        this.pendingShaderLoads = 0;

        this.texturesToCreate = new Queue();
        this.pendingTextureLoads = 0;
    }

    LoadResources.prototype.finishedPendingLoads = function() {
        return ((this.pendingBufferLoads === 0) &&
                (this.pendingShaderLoads === 0) &&
                (this.pendingTextureLoads === 0));
    };

    LoadResources.prototype.finishedResourceCreeation = function() {
        return ((this.bufferViewsToCreate.length === 0) &&
                (this.programsToCreate.length === 0) &&
                (this.texturesToCreate.length === 0));
    };

    /**
     * DOC_TBA
     *
     * @alias Model
     * @constructor
     */
    var Model = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        /**
         * DOC_TBA
         */
        this.json = options.json;

        /**
         * DOC_TBA
         */
        this.basePath = defaultValue(options.basePath, '');

        /**
         * Determines if the model primitive will be shown.
         *
         * @type {Boolean}
         *
         * @default true
         */
        this.show = true;

        this._state = ModelState.NEEDS_LOAD;
        this._loadResources = undefined;
    };

    ///////////////////////////////////////////////////////////////////////////

    function failedLoad() {
        // TODO
    }

    function bufferLoad(model, name) {
        return function(arrayBuffer) {
            var loadResources = model._loadResources;
            loadResources.buffers[name] = arrayBuffer;
            --loadResources.pendingBufferLoads;
         };
    }

    function parseBuffers(model) {
        var buffers = model.json.buffers;
        for (var name in buffers) {
            if (buffers.hasOwnProperty(name)) {
                ++model._loadResources.pendingBufferLoads;
                var bufferPath = model.basePath + buffers[name].path;
                loadArrayBuffer(bufferPath).then(bufferLoad(model, name), failedLoad);
            }
        }
    }

    function parseBufferViews(model) {
        var bufferViews = model.json.bufferViews;
        for (var name in bufferViews) {
            if (bufferViews.hasOwnProperty(name)) {
                model._loadResources.bufferViewsToCreate.enqueue(name);
            }
        }
    }

    function shaderLoad(model, name) {
        return function(source) {
            var loadResources = model._loadResources;
            loadResources.shaders[name] = source;
            --loadResources.pendingShaderLoads;
         };
    }

    function parseShaders(model) {
        var shaders = model.json.shaders;
        for (var name in shaders) {
            if (shaders.hasOwnProperty(name)) {
                ++model._loadResources.pendingShaderLoads;
                var shaderPath = model.basePath + shaders[name].path;
                loadText(shaderPath).then(shaderLoad(model, name), failedLoad);
            }
        }
    }

    function parsePrograms(model) {
        var programs = model.json.programs;
        for (var name in programs) {
            if (programs.hasOwnProperty(name)) {
                model._loadResources.programsToCreate.enqueue(name);
            }
        }
    }

    function imageLoad(model, name) {
        return function(image) {
            var loadResources = model._loadResources;
            --loadResources.pendingTextureLoads;
            loadResources.texturesToCreate.enqueue({
                 name : name,
                 image : image
             });
         };
    }

    function parseImages(model) {
        var images = model.json.images;
        for (var name in images) {
            if (images.hasOwnProperty(name)) {
                ++model._loadResources.pendingTextureLoads;
                var imagePath = model.basePath + images[name].path;
                loadImage(imagePath).then(imageLoad(model, name), failedLoad);
            }
        }
    }

    function parseJson(model) {
        parseBuffers(model);
        parseBufferViews(model);
        parseShaders(model);
        parsePrograms(model);
        parseImages(model);
    }

    ///////////////////////////////////////////////////////////////////////////

    function createBufferViews(model, context) {
        var loadResources = model._loadResources;

// TODO: more fine-grained bufferView-to-buffer dependencies
        if (loadResources.pendingBufferLoads !== 0) {
            return;
        }

        var raw;
        var bufferView;
        var bufferViews = model.json.bufferViews;
        var buffers = loadResources.buffers;

        while (loadResources.bufferViewsToCreate.length > 0) {
            var bufferViewName = loadResources.bufferViewsToCreate.dequeue();
            bufferView = bufferViews[bufferViewName];
            bufferView.extra = defaultValue(bufferView.extra, {});

            if (bufferView.target === 'ARRAY_BUFFER') {
                // Only ARRAY_BUFFER here.  ELEMENT_ARRAY_BUFFER created below.
                raw = new Uint8Array(buffers[bufferView.buffer], bufferView.byteOffset, bufferView.byteLength);
                bufferView.extra.czmBuffer = context.createVertexBuffer(raw, BufferUsage.STATIC_DRAW);
            }
        }

        // The Cesium Renderer requires knowing the datatype for an index buffer
        // at creation type, which is not part of the glTF bufferview so loop
        // through glTF indices to create the bufferview's index buffer.
        var indices = model.json.indices;
        for (var name in indices) {
            if (indices.hasOwnProperty(name)) {
                var instance = indices[name];
                bufferView = bufferViews[instance.bufferView];

                if (!defined(bufferView.extra.czmBuffer)) {
                    raw = new Uint8Array(buffers[bufferView.buffer], bufferView.byteOffset, bufferView.byteLength);
                    bufferView.extra.czmBuffer = context.createIndexBuffer(raw, BufferUsage.STATIC_DRAW, IndexDatatype[instance.type]);
                    // In theory, several glTF indices with different types could
                    // point to the same glTF bufferView, which would break this.
                    // In practice, it is unlikely as it will be UNSIGNED_SHORT.
                }
            }
        }
    }

    function createPrograms(model, context) {
        var loadResources = model._loadResources;

// TODO: more fine-grained program-to-shader dependencies
        if (loadResources.pendingShaderLoads !== 0) {
            return;
        }

        var programs = model.json.programs;
        var shaders = loadResources.shaders;

        // Create one program per frame
        if (loadResources.programsToCreate.length > 0) {
            var name = loadResources.programsToCreate.dequeue();
            var program = programs[name];
            program.extra = defaultValue(program.extra, {});
            program.extra.czmProgram = context.getShaderCache().getShaderProgram(
                shaders[program.vertexShader],
                shaders[program.fragmentShader]);
        }
    }

    function createTextures(model, context) {
        var loadResources = model._loadResources;
        var images = model.json.images;

        // Create one texture per frame
        if (loadResources.texturesToCreate.length > 0) {
            var textureToCreate = loadResources.texturesToCreate.dequeue();

            var image = images[textureToCreate.name];
            image.extra = defaultValue(image.extra, {});
            image.extra.czmTexture = context.createTexture2D({
                source : textureToCreate.image,
                flipY : false
            });
// TODO: texture cache
        }
    }

    function createResources(model, context) {
        createBufferViews(model, context);
        createPrograms(model, context);
        createTextures(model, context);
    }

    ///////////////////////////////////////////////////////////////////////////

    /**
     * @private
     */
    Model.prototype.update = function(context, frameState, commandList) {
        if (!this.show ||
            (frameState.mode !== SceneMode.SCENE3D)) {
// TODO: models in 2D and Columbus view
            return;
        }

        if ((this._state === ModelState.NEEDS_LOAD) && defined(this.json)) {
            this._state = ModelState.LOADING;
            this._loadResources = new LoadResources();
            parseJson(this);
        }

        if (this._state === ModelState.LOADING) {
            // Incrementally create WebGL resources as buffers/shaders/textures are downloaded
            createResources(this, context);

            var loadResources = this._loadResources;
            if (loadResources.finishedPendingLoads() && loadResources.finishedResourceCreeation()) {
                this._state = ModelState.LOADED;
                this._loadResources = undefined;  // Clear CPU memory since WebGL resources were created.
            }
        }
    };

    /**
     * Returns true if this object was destroyed; otherwise, false.
     *

     * If this object was destroyed, it should not be used; calling any function other than
     * isDestroyed will result in a {@link DeveloperError} exception.
     *
     * @memberof Model
     *
     * @return {Boolean} true if this object was destroyed; otherwise, false.
     *
     * @see Model#destroy
     */
    Model.prototype.isDestroyed = function() {
        return false;
    };

    function destroyExtra(property, resourceName) {
        for (var name in property) {
            if (property.hasOwnProperty(name)) {
                var extra = property[name].extra;
                if (defined(extra) && defined(extra[resourceName])) {
                    extra[resourceName] = extra[resourceName].destroy();
                }
            }
        }
    }

    /**
     * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
     * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
     *

     * Once an object is destroyed, it should not be used; calling any function other than
     * isDestroyed will result in a {@link DeveloperError} exception.  Therefore,
     * assign the return value (undefined) to the object as done in the example.
     *
     * @memberof Model
     *
     * @return {undefined}
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @see Model#isDestroyed
     *
     * @example
     * model = model && model.destroy();
     */
    Model.prototype.destroy = function() {
        var json = this.json;
        destroyExtra(json.buffers, 'czmBuffer');
        destroyExtra(json.program, 'czmProgram');
        destroyExtra(json.images, 'czmTexture');

        return destroyObject(this);
    };

    return Model;
});
<commit_msg>Added VertexArray creation<commit_after>/*global define*/
define([
        '../Core/defined',
        '../Core/defaultValue',
        '../Core/destroyObject',
        '../Core/Enumeration',
        '../Core/loadArrayBuffer',
        '../Core/loadText',
        '../Core/loadImage',
        '../Core/Queue',
        '../Core/IndexDatatype',
        '../Core/ComponentDatatype',
        '../Renderer/BufferUsage',
        './SceneMode'
    ], function(
        defined,
        defaultValue,
        destroyObject,
        Enumeration,
        loadArrayBuffer,
        loadText,
        loadImage,
        Queue,
        IndexDatatype,
        ComponentDatatype,
        BufferUsage,
        SceneMode) {
    "use strict";
// TODO: remove before merge to master
/*global console*/

    var ModelState = {
        NEEDS_LOAD : new Enumeration(0, 'NEEDS_LOAD'),
        LOADING : new Enumeration(1, 'LOADING'),
        LOADED : new Enumeration(2, 'LOADED')
    };

    function LoadResources() {
        this.bufferViewsToCreate = new Queue();
        this.buffers = {};
        this.pendingBufferLoads = 0;

        this.programsToCreate = new Queue();
        this.shaders = {};
        this.pendingShaderLoads = 0;

        this.texturesToCreate = new Queue();
        this.pendingTextureLoads = 0;
    }

    LoadResources.prototype.finishedPendingLoads = function() {
        return ((this.pendingBufferLoads === 0) &&
                (this.pendingShaderLoads === 0) &&
                (this.pendingTextureLoads === 0));
    };

    LoadResources.prototype.finishedResourceCreation = function() {
        return ((this.bufferViewsToCreate.length === 0) &&
                (this.programsToCreate.length === 0) &&
                (this.texturesToCreate.length === 0));
    };

    LoadResources.prototype.finishedBufferViewsCreation = function() {
        return ((this.pendingBufferLoads === 0) && (this.bufferViewsToCreate.length === 0));
    };

    /**
     * DOC_TBA
     *
     * @alias Model
     * @constructor
     */
    var Model = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        /**
         * DOC_TBA
         */
        this.json = options.json;

        /**
         * DOC_TBA
         */
        this.basePath = defaultValue(options.basePath, '');

        /**
         * Determines if the model primitive will be shown.
         *
         * @type {Boolean}
         *
         * @default true
         */
        this.show = true;

        this._state = ModelState.NEEDS_LOAD;
        this._loadResources = undefined;
    };

    ///////////////////////////////////////////////////////////////////////////

    function failedLoad() {
        // TODO
    }

    function bufferLoad(model, name) {
        return function(arrayBuffer) {
            var loadResources = model._loadResources;
            loadResources.buffers[name] = arrayBuffer;
            --loadResources.pendingBufferLoads;
         };
    }

    function parseBuffers(model) {
        var buffers = model.json.buffers;
        for (var name in buffers) {
            if (buffers.hasOwnProperty(name)) {
                ++model._loadResources.pendingBufferLoads;
                var bufferPath = model.basePath + buffers[name].path;
                loadArrayBuffer(bufferPath).then(bufferLoad(model, name), failedLoad);
            }
        }
    }

    function parseBufferViews(model) {
        var bufferViews = model.json.bufferViews;
        for (var name in bufferViews) {
            if (bufferViews.hasOwnProperty(name)) {
                model._loadResources.bufferViewsToCreate.enqueue(name);
            }
        }
    }

    function shaderLoad(model, name) {
        return function(source) {
            var loadResources = model._loadResources;
            loadResources.shaders[name] = source;
            --loadResources.pendingShaderLoads;
         };
    }

    function parseShaders(model) {
        var shaders = model.json.shaders;
        for (var name in shaders) {
            if (shaders.hasOwnProperty(name)) {
                ++model._loadResources.pendingShaderLoads;
                var shaderPath = model.basePath + shaders[name].path;
                loadText(shaderPath).then(shaderLoad(model, name), failedLoad);
            }
        }
    }

    function parsePrograms(model) {
        var programs = model.json.programs;
        for (var name in programs) {
            if (programs.hasOwnProperty(name)) {
                model._loadResources.programsToCreate.enqueue(name);
            }
        }
    }

    function imageLoad(model, name) {
        return function(image) {
            var loadResources = model._loadResources;
            --loadResources.pendingTextureLoads;
            loadResources.texturesToCreate.enqueue({
                 name : name,
                 image : image
             });
         };
    }

    function parseImages(model) {
        var images = model.json.images;
        for (var name in images) {
            if (images.hasOwnProperty(name)) {
                ++model._loadResources.pendingTextureLoads;
                var imagePath = model.basePath + images[name].path;
                loadImage(imagePath).then(imageLoad(model, name), failedLoad);
            }
        }
    }

    function parseJson(model) {
        parseBuffers(model);
        parseBufferViews(model);
        parseShaders(model);
        parsePrograms(model);
        parseImages(model);
    }

    ///////////////////////////////////////////////////////////////////////////

    function createBuffers(model, context) {
        var loadResources = model._loadResources;

// TODO: more fine-grained bufferView-to-buffer dependencies
        if (loadResources.pendingBufferLoads !== 0) {
            return;
        }

        var raw;
        var bufferView;
        var bufferViews = model.json.bufferViews;
        var buffers = loadResources.buffers;

        while (loadResources.bufferViewsToCreate.length > 0) {
            var bufferViewName = loadResources.bufferViewsToCreate.dequeue();
            bufferView = bufferViews[bufferViewName];
            bufferView.extra = defaultValue(bufferView.extra, {});

            if (bufferView.target === 'ARRAY_BUFFER') {
                // Only ARRAY_BUFFER here.  ELEMENT_ARRAY_BUFFER created below.
                raw = new Uint8Array(buffers[bufferView.buffer], bufferView.byteOffset, bufferView.byteLength);
                var vertexBuffer = context.createVertexBuffer(raw, BufferUsage.STATIC_DRAW);
                vertexBuffer.setVertexArrayDestroyable(false);
                bufferView.extra.czmBuffer = vertexBuffer;
            }
        }

        // The Cesium Renderer requires knowing the datatype for an index buffer
        // at creation type, which is not part of the glTF bufferview so loop
        // through glTF indices to create the bufferview's index buffer.
        var indices = model.json.indices;
        for (var name in indices) {
            if (indices.hasOwnProperty(name)) {
                var instance = indices[name];
                bufferView = bufferViews[instance.bufferView];

                if (!defined(bufferView.extra.czmBuffer)) {
                    raw = new Uint8Array(buffers[bufferView.buffer], bufferView.byteOffset, bufferView.byteLength);
                    var indexBuffer = context.createIndexBuffer(raw, BufferUsage.STATIC_DRAW, IndexDatatype[instance.type]);
                    indexBuffer.setVertexArrayDestroyable(false);
                    bufferView.extra.czmBuffer = indexBuffer;
                    // In theory, several glTF indices with different types could
                    // point to the same glTF bufferView, which would break this.
                    // In practice, it is unlikely as it will be UNSIGNED_SHORT.
                }
            }
        }
    }

    function createPrograms(model, context) {
        var loadResources = model._loadResources;

// TODO: more fine-grained program-to-shader dependencies
        if (loadResources.pendingShaderLoads !== 0) {
            return;
        }

        var programs = model.json.programs;
        var shaders = loadResources.shaders;

        // Create one program per frame
        if (loadResources.programsToCreate.length > 0) {
            var name = loadResources.programsToCreate.dequeue();
            var program = programs[name];
            program.extra = defaultValue(program.extra, {});
            program.extra.czmProgram = context.getShaderCache().getShaderProgram(
                shaders[program.vertexShader],
                shaders[program.fragmentShader]);
        }
    }

    function createTextures(model, context) {
        var loadResources = model._loadResources;
        var images = model.json.images;

        // Create one texture per frame
        if (loadResources.texturesToCreate.length > 0) {
            var textureToCreate = loadResources.texturesToCreate.dequeue();

            var image = images[textureToCreate.name];
            image.extra = defaultValue(image.extra, {});
            image.extra.czmTexture = context.createTexture2D({
                source : textureToCreate.image,
                flipY : false
            });
// TODO: texture cache
        }
    }

    var gltfTypes = {
        FLOAT : {
            componentsPerAttribute : 1,
            componentDatatype : ComponentDatatype.FLOAT
        },
        FLOAT_VEC2 : {
            componentsPerAttribute : 2,
            componentDatatype : ComponentDatatype.FLOAT
        },
        FLOAT_VEC3 : {
            componentsPerAttribute : 3,
            componentDatatype : ComponentDatatype.FLOAT
        },
        FLOAT_VEC4 : {
            componentsPerAttribute : 4,
            componentDatatype : ComponentDatatype.FLOAT
        }
// TODO: add other types
    };

    function getSemanticToAttributeLocations(model, primitive) {
// TODO: this could be done per material, not per mesh, if we don't change glTF
        var programs = model.json.programs;
        var techniques = model.json.techniques;
        var materials = model.json.materials;

        // Retrieve the compiled shader program to assign index values to attributes
        var semanticToAttributeLocations = {};

        var technique = techniques[materials[primitive.material].instanceTechnique.technique];
        var parameters = technique.parameters;
        var pass = technique.passes[technique.pass];
        var instanceProgram = pass.instanceProgram;
        var program = programs[instanceProgram.program];
        var attributes = instanceProgram.attributes;
        var attributeLocations = program.extra.czmProgram.getVertexAttributes();

        for (var name in attributes) {
            if (attributes.hasOwnProperty(name)) {
                var parameter = parameters[attributes[name]];

                semanticToAttributeLocations[parameter.semantic] = attributeLocations[name].index;
            }
        }

        return semanticToAttributeLocations;
    }

    function createVertexArrays(model, context) {
        var loadResources = model._loadResources;

// TODO: more fine-grained mesh-to-buffer-views dependencies
         if (!loadResources.finishedBufferViewsCreation()) {
             return;
         }

         var bufferViews = model.json.bufferViews;
         var attributes = model.json.attributes;
         var indices = model.json.indices;
         var meshes = model.json.meshes;
         var name;

         for (name in meshes) {
             if (meshes.hasOwnProperty(name)) {
                 var primitives = meshes[name].primitives;

                 for (name in primitives) {
                     if (primitives.hasOwnProperty(name)) {
                         var primitive = primitives[name];

                         var semanticToAttributeLocations = getSemanticToAttributeLocations(model, primitive);
                         var attrs = [];
                         var semantics = primitive.semantics;
                         for (name in semantics) {
                             if (semantics.hasOwnProperty(name)) {
                                 var a = attributes[semantics[name]];

                                 var type = gltfTypes[a.type];
                                 attrs.push({
                                     index                  : semanticToAttributeLocations[name],
                                     vertexBuffer           : bufferViews[a.bufferView].extra.czmBuffer,
                                     componentsPerAttribute : type.componentsPerAttribute,
                                     componentDatatype      : type.componentDatatype,
// TODO: is normalize part of glTF attribute?
                                     normalize              : false,
                                     offsetInBytes          : a.byteOffset,
                                     strideInBytes          : a.byteStride
                                 });
                             }
                         }

                         var i = indices[primitive.indices];
                         var indexBuffer = bufferViews[i.bufferView].extra.czmBuffer;

                         primitive.extra = defaultValue(primitive.extra, {});
                         primitive.extra.czmVertexArray = context.createVertexArray(attrs, indexBuffer);
                     }
                 }
             }
         }
    }

    function createResources(model, context) {
        createBuffers(model, context);      // using glTF bufferViews
        createPrograms(model, context);
        createTextures(model, context);

        createVertexArrays(model, context); // using glTF meshes
    }

    ///////////////////////////////////////////////////////////////////////////

    /**
     * @private
     */
    Model.prototype.update = function(context, frameState, commandList) {
        if (!this.show ||
            (frameState.mode !== SceneMode.SCENE3D)) {
// TODO: models in 2D and Columbus view
            return;
        }

        if ((this._state === ModelState.NEEDS_LOAD) && defined(this.json)) {
            this._state = ModelState.LOADING;
            this._loadResources = new LoadResources();
            parseJson(this);
        }

        if (this._state === ModelState.LOADING) {
            // Incrementally create WebGL resources as buffers/shaders/textures are downloaded
            createResources(this, context);

            var loadResources = this._loadResources;
            if (loadResources.finishedPendingLoads() && loadResources.finishedResourceCreation()) {
                this._state = ModelState.LOADED;
                this._loadResources = undefined;  // Clear CPU memory since WebGL resources were created.
            }
        }
    };

    /**
     * Returns true if this object was destroyed; otherwise, false.
     *

     * If this object was destroyed, it should not be used; calling any function other than
     * isDestroyed will result in a {@link DeveloperError} exception.
     *
     * @memberof Model
     *
     * @return {Boolean} true if this object was destroyed; otherwise, false.
     *
     * @see Model#destroy
     */
    Model.prototype.isDestroyed = function() {
        return false;
    };

    function destroyExtra(property, resourceName) {
        for (var name in property) {
            if (property.hasOwnProperty(name)) {
                var extra = property[name].extra;
                if (defined(extra) && defined(extra[resourceName])) {
                    extra[resourceName] = extra[resourceName].destroy();
                }
            }
        }
    }

    /**
     * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
     * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
     *

     * Once an object is destroyed, it should not be used; calling any function other than
     * isDestroyed will result in a {@link DeveloperError} exception.  Therefore,
     * assign the return value (undefined) to the object as done in the example.
     *
     * @memberof Model
     *
     * @return {undefined}
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @see Model#isDestroyed
     *
     * @example
     * model = model && model.destroy();
     */
    Model.prototype.destroy = function() {
        var json = this.json;
        destroyExtra(json.bufferViews, 'czmBuffer');
        destroyExtra(json.program, 'czmProgram');
        destroyExtra(json.images, 'czmTexture');

        return destroyObject(this);
    };

    return Model;
});
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

import { regular } from './icon-definitions';

const StyledIcon = styled.i`
  display: inline-block;
  font-family: ${props => props.fontFamily} !important;
  font-size: ${props => props.fontSize};
  font-style: normal;
  font-weight: inherit;
  font-variant: normal;
  speak: none;
  text-decoration: none;
  text-transform: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  vertical-align: text-bottom;
  line-height: 1;

  &:before {
    content: "\\e${props => props.content}";
  }
`;

function Icon({ name, size, ...other }) {
  const iconName = `im-icon-${name}`;

  const styledIconProps = {
    content: regular[iconName],
    fontFamily: 'indv-mkt-icons',
    fontSize: size !== undefined ? `${size}px` : 'inherit'
  };

  return <StyledIcon aria-hidden {...styledIconProps} {...other} />;
}

Icon.propTypes = {
  /** Name of the icon to display */
  name: PropTypes.string.isRequired,
  /** Specify icon size in pixels */
  size: PropTypes.number
};

Icon.defaultProps = {
  size: undefined
};

export default Icon;
<commit_msg>Fix: Move line-height property to correct location<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

import { regular } from './icon-definitions';

const StyledIcon = styled.i`
  display: inline-block;
  font-family: ${props => props.fontFamily} !important;
  font-size: ${props => props.fontSize};
  font-style: normal;
  font-weight: inherit;
  font-variant: normal;
  line-height: 1;
  speak: none;
  text-decoration: none;
  text-transform: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  vertical-align: text-bottom;

  &:before {
    content: "\\e${props => props.content}";
  }
`;

function Icon({ name, size, ...other }) {
  const iconName = `im-icon-${name}`;

  const styledIconProps = {
    content: regular[iconName],
    fontFamily: 'indv-mkt-icons',
    fontSize: size !== undefined ? `${size}px` : 'inherit'
  };

  return <StyledIcon aria-hidden {...styledIconProps} {...other} />;
}

Icon.propTypes = {
  /** Name of the icon to display */
  name: PropTypes.string.isRequired,
  /** Specify icon size in pixels */
  size: PropTypes.number
};

Icon.defaultProps = {
  size: undefined
};

export default Icon;
<|endoftext|>