<commit_before>describe('sorting lines', () => {
  let activationPromise, editor, editorView

  const sortLines = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:sort')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  const sortLinesReversed = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:reverse-sort')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  const uniqueLines = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:unique')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  const sortLineCaseInsensitive = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:case-insensitive-sort')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  const sortLinesNatural = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:natural')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  beforeEach(() => {
    waitsForPromise(() => atom.workspace.open())

    runs(() => {
      editor = atom.workspace.getActiveTextEditor()
      editorView = atom.views.getView(editor)
      activationPromise = atom.packages.activatePackage('sort-lines')
    })
  })

  describe('when no lines are selected', () => {
    it('sorts all lines', () => {
      editor.setText(
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium    '
      )
      editor.setCursorBufferPosition([0, 0])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Helium   \n' +
          'Hydrogen \n' +
          'Lithium    '
        )
      )
    })

    it('sorts all lines, ignoring the trailing new line', () => {
      editor.setText(
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Helium   \n' +
          'Hydrogen \n' +
          'Lithium  \n'
        )
      )
    })

    it('gracefully handles attempt to sort an empty editor', () => {
      editor.setText('')
      editor.setCursorBufferPosition([0, 0])

      sortLines(() => expect(editor.getText()).toBe(''))
    })
  })

  describe('when entire lines are selected', () =>
    it('sorts the selected lines', () => {
      editor.setText(
        'Hydrogen  \n' +
        'Helium    \n' +
        'Lithium   \n' +
        'Beryllium \n' +
        'Boron     \n'
      )
      editor.setSelectedBufferRange([[1, 0], [4, 0]])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen  \n' +
          'Beryllium \n' +
          'Helium    \n' +
          'Lithium   \n' +
          'Boron     \n'
        )
      )
    })
  )

  describe('when partial lines are selected', () =>
    it('sorts the selected lines', () => {
      editor.setText(
        'Hydrogen  \n' +
        'Helium    \n' +
        'Lithium   \n' +
        'Beryllium \n' +
        'Boron     \n'
      )
      editor.setSelectedBufferRange([[1, 3], [3, 2]])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen  \n' +
          'Beryllium \n' +
          'Helium    \n' +
          'Lithium   \n' +
          'Boron     \n'
        )
      )
    })
  )

  describe('when there are multiple selection ranges', () =>
    it('sorts the lines in each selection range', () => {
      editor.setText(
        'Hydrogen                \n' +
        'Helium    # selection 1 \n' +
        'Beryllium # selection 1 \n' +
        'Carbon                  \n' +
        'Fluorine  # selection 2 \n' +
        'Aluminum  # selection 2 \n' +
        'Gallium                 \n' +
        'Europium                \n'
      )
      editor.addSelectionForBufferRange([[1, 0], [3, 0]])
      editor.addSelectionForBufferRange([[4, 0], [6, 0]])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen                \n' +
          'Beryllium # selection 1 \n' +
          'Helium    # selection 1 \n' +
          'Carbon                  \n' +
          'Aluminum  # selection 2 \n' +
          'Fluorine  # selection 2 \n' +
          'Gallium                 \n' +
          'Europium                \n'
        )
      )
    })
  )

  describe('reversed sorting', () =>
    it('sorts all lines in reverse order', () => {
      editor.setText(
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesReversed(() =>
        expect(editor.getText()).toBe(
          'Lithium  \n' +
          'Hydrogen \n' +
          'Helium   \n'
        )
      )
    })
  )

  describe('uniqueing', () => {
    it('uniques all lines but does not change order', () => {
      editor.setText(
        'Hydrogen \n' +
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n' +
        'Hydrogen \n' +
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n' +
        'Hydrogen \n' +
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n' +
        'Hydrogen \n' +
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      uniqueLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen \n' +
          'Helium   \n' +
          'Lithium  \n'
        )
      )
    })

    it('uniques all lines using CRLF line-endings', () => {
      editor.setText(
        'Hydrogen\r\n' +
        'Hydrogen\r\n' +
        'Helium\r\n'   +
        'Lithium\r\n'  +
        'Hydrogen\r\n' +
        'Hydrogen\r\n' +
        'Helium\r\n'   +
        'Lithium\r\n'  +
        'Hydrogen\r\n' +
        'Hydrogen\r\n' +
        'Helium\r\n'   +
        'Lithium\r\n'  +
        'Hydrogen\r\n' +
        'Hydrogen\r\n' +
        'Helium\r\n'   +
        'Lithium\r\n'
      )
      editor.setCursorBufferPosition([0, 0])

      uniqueLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen\r\n' +
          'Helium\r\n'   +
          'Lithium\r\n'
        )
      )
    })
  })

  describe('case-insensitive sorting', () =>
    it('sorts all lines, ignoring case', () => {
      editor.setText(
        'Hydrogen \n' +
        'lithium  \n' +
        'helium   \n' +
        'Helium   \n' +
        'Lithium  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLineCaseInsensitive(() =>
        expect(editor.getText()).toBe(
          'helium   \n' +
          'Helium   \n' +
          'Hydrogen \n' +
          'lithium  \n' +
          'Lithium  \n'
        )
      )
    })
  )

  describe('natural sorting', () => {
    it('orders by leading numerals', () => {
      editor.setText(
        '4a  \n' +
        '1a  \n' +
        '2a  \n' +
        '12a \n' +
        '3a  \n' +
        '0a  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '0a  \n' +
          '1a  \n' +
          '2a  \n' +
          '3a  \n' +
          '4a  \n' +
          '12a \n'
        )
      )
    })

    it('orders by word', () => {
      editor.setText(
        '1Hydrogen1  \n' +
        '1Beryllium1 \n' +
        '1Carbon1    \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '1Beryllium1 \n' +
          '1Carbon1    \n' +
          '1Hydrogen1  \n'
        )
      )
    })

    it('orders by trailing numeral', () => {
      editor.setText(
        'a4  \n' +
        'a0  \n' +
        'a12 \n' +
        'a1  \n' +
        'a2  \n' +
        'a3  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          'a0  \n' +
          'a1  \n' +
          'a2  \n' +
          'a3  \n' +
          'a4  \n' +
          'a12 \n'
        )
      )
    })

    it('orders by leading numeral before word', () => {
      editor.setText(
        '4b \n' +
        '3a \n' +
        '2b \n' +
        '1a \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '1a \n' +
          '2b \n' +
          '3a \n' +
          '4b \n'
        )
      )
    })

    it('orders by word before trailing number', () => {
      editor.setText(
        'c2 \n' +
        'a4 \n' +
        'd1 \n' +
        'b3 \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          'a4 \n' +
          'b3 \n' +
          'c2 \n' +
          'd1 \n'
        )
      )
    })

    it('properly handles leading zeros', () => {
      editor.setText(
        'a01  \n' +
        'a001 \n' +
        'a003 \n' +
        'a002 \n' +
        'a02  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          'a001 \n' +
          'a002 \n' +
          'a003 \n' +
          'a01  \n' +
          'a02  \n'
        )
      )
    })

    it('properly handles simple numerics', () => {
      editor.setText(
        '10 \n' +
        '9  \n' +
        '2  \n' +
        '1  \n' +
        '4  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '1  \n' +
          '2  \n' +
          '4  \n' +
          '9  \n' +
          '10 \n'
        )
      )
    })

    it('properly handles floats', () => {
      editor.setText(
        '10.0401   \n' +
        '10.022    \n' +
        '10.042    \n' +
        '10.021999 \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '10.021999 \n' +
          '10.022    \n' +
          '10.0401   \n' +
          '10.042    \n'
        )
      )
    })

    it('properly handles float & decimal notation', () => {
      editor.setText(
        '10.04f  \n' +
        '10.039F \n' +
        '10.038d \n' +
        '10.037D \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
        '10.037D \n' +
        '10.038d \n' +
        '10.039F \n' +
        '10.04f  \n'
        )
      )
    })

    it('properly handles scientific notation', () => {
      editor.setText(
        '1.528535048e5 \n' +
        '1.528535047e7 \n' +
        '1.528535049e3 \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '1.528535049e3 \n' +
          '1.528535048e5 \n' +
          '1.528535047e7 \n'
        )
      )
    })

    it('properly handles ip addresses', () => {
      editor.setText(
        '192.168.0.100 \n' +
        '192.168.0.1   \n' +
        '192.168.1.1   \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '192.168.0.1   \n' +
          '192.168.0.100 \n' +
          '192.168.1.1   \n'
        )
      )
    })

    it('properly handles filenames', () => {
      editor.setText(
        'car.mov             \n' +
        '01alpha.sgi         \n' +
        '001alpha.sgi        \n' +
        'my.string_41299.tif \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '001alpha.sgi        \n' +
          '01alpha.sgi         \n' +
          'car.mov             \n' +
          'my.string_41299.tif \n'
        )
      )
    })

    it('properly handles dates', () => {
      editor.setText(
        '10/12/2008 \n' +
        '10/11/2008 \n' +
        '10/11/2007 \n' +
        '10/12/2007 \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '10/11/2007 \n' +
          '10/12/2007 \n' +
          '10/11/2008 \n' +
          '10/12/2008 \n'
        )
      )
    })

    it('properly handles money', () => {
      editor.setText(
        '$10002.00 \n' +
        '$10001.02 \n' +
        '$10001.01 \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '$10001.01 \n' +
          '$10001.02 \n' +
          '$10002.00 \n'
        )
      )
    })
  })
})
<commit_msg>Remove unnecessary test setup<commit_after>describe('sorting lines', () => {
  let activationPromise, editor, editorView

  const sortLines = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:sort')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  const sortLinesReversed = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:reverse-sort')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  const uniqueLines = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:unique')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  const sortLineCaseInsensitive = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:case-insensitive-sort')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  const sortLinesNatural = (callback) => {
    atom.commands.dispatch(editorView, 'sort-lines:natural')
    waitsForPromise(() => activationPromise)
    runs(callback)
  }

  beforeEach(() => {
    waitsForPromise(() => atom.workspace.open())

    runs(() => {
      editor = atom.workspace.getActiveTextEditor()
      editorView = atom.views.getView(editor)
      activationPromise = atom.packages.activatePackage('sort-lines')
    })
  })

  describe('when no lines are selected', () => {
    it('sorts all lines', () => {
      editor.setText(
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium    '
      )
      editor.setCursorBufferPosition([0, 0])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Helium   \n' +
          'Hydrogen \n' +
          'Lithium    '
        )
      )
    })

    it('sorts all lines, ignoring the trailing new line', () => {
      editor.setText(
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n'
      )
      editor.setCursorBufferPosition([0, 0])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Helium   \n' +
          'Hydrogen \n' +
          'Lithium  \n'
        )
      )
    })

    it('gracefully handles attempt to sort an empty editor', () => {
      editor.setText('')
      editor.setCursorBufferPosition([0, 0])

      sortLines(() => expect(editor.getText()).toBe(''))
    })
  })

  describe('when entire lines are selected', () =>
    it('sorts the selected lines', () => {
      editor.setText(
        'Hydrogen  \n' +
        'Helium    \n' +
        'Lithium   \n' +
        'Beryllium \n' +
        'Boron     \n'
      )
      editor.setSelectedBufferRange([[1, 0], [4, 0]])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen  \n' +
          'Beryllium \n' +
          'Helium    \n' +
          'Lithium   \n' +
          'Boron     \n'
        )
      )
    })
  )

  describe('when partial lines are selected', () =>
    it('sorts the selected lines', () => {
      editor.setText(
        'Hydrogen  \n' +
        'Helium    \n' +
        'Lithium   \n' +
        'Beryllium \n' +
        'Boron     \n'
      )
      editor.setSelectedBufferRange([[1, 3], [3, 2]])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen  \n' +
          'Beryllium \n' +
          'Helium    \n' +
          'Lithium   \n' +
          'Boron     \n'
        )
      )
    })
  )

  describe('when there are multiple selection ranges', () =>
    it('sorts the lines in each selection range', () => {
      editor.setText(
        'Hydrogen                \n' +
        'Helium    # selection 1 \n' +
        'Beryllium # selection 1 \n' +
        'Carbon                  \n' +
        'Fluorine  # selection 2 \n' +
        'Aluminum  # selection 2 \n' +
        'Gallium                 \n' +
        'Europium                \n'
      )
      editor.addSelectionForBufferRange([[1, 0], [3, 0]])
      editor.addSelectionForBufferRange([[4, 0], [6, 0]])

      sortLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen                \n' +
          'Beryllium # selection 1 \n' +
          'Helium    # selection 1 \n' +
          'Carbon                  \n' +
          'Aluminum  # selection 2 \n' +
          'Fluorine  # selection 2 \n' +
          'Gallium                 \n' +
          'Europium                \n'
        )
      )
    })
  )

  describe('reversed sorting', () =>
    it('sorts all lines in reverse order', () => {
      editor.setText(
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n'
      )

      sortLinesReversed(() =>
        expect(editor.getText()).toBe(
          'Lithium  \n' +
          'Hydrogen \n' +
          'Helium   \n'
        )
      )
    })
  )

  describe('uniqueing', () => {
    it('uniques all lines but does not change order', () => {
      editor.setText(
        'Hydrogen \n' +
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n' +
        'Hydrogen \n' +
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n' +
        'Hydrogen \n' +
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n' +
        'Hydrogen \n' +
        'Hydrogen \n' +
        'Helium   \n' +
        'Lithium  \n'
      )

      uniqueLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen \n' +
          'Helium   \n' +
          'Lithium  \n'
        )
      )
    })

    it('uniques all lines using CRLF line-endings', () => {
      editor.setText(
        'Hydrogen\r\n' +
        'Hydrogen\r\n' +
        'Helium\r\n'   +
        'Lithium\r\n'  +
        'Hydrogen\r\n' +
        'Hydrogen\r\n' +
        'Helium\r\n'   +
        'Lithium\r\n'  +
        'Hydrogen\r\n' +
        'Hydrogen\r\n' +
        'Helium\r\n'   +
        'Lithium\r\n'  +
        'Hydrogen\r\n' +
        'Hydrogen\r\n' +
        'Helium\r\n'   +
        'Lithium\r\n'
      )

      uniqueLines(() =>
        expect(editor.getText()).toBe(
          'Hydrogen\r\n' +
          'Helium\r\n'   +
          'Lithium\r\n'
        )
      )
    })
  })

  describe('case-insensitive sorting', () =>
    it('sorts all lines, ignoring case', () => {
      editor.setText(
        'Hydrogen \n' +
        'lithium  \n' +
        'helium   \n' +
        'Helium   \n' +
        'Lithium  \n'
      )

      sortLineCaseInsensitive(() =>
        expect(editor.getText()).toBe(
          'helium   \n' +
          'Helium   \n' +
          'Hydrogen \n' +
          'lithium  \n' +
          'Lithium  \n'
        )
      )
    })
  )

  describe('natural sorting', () => {
    it('orders by leading numerals', () => {
      editor.setText(
        '4a  \n' +
        '1a  \n' +
        '2a  \n' +
        '12a \n' +
        '3a  \n' +
        '0a  \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '0a  \n' +
          '1a  \n' +
          '2a  \n' +
          '3a  \n' +
          '4a  \n' +
          '12a \n'
        )
      )
    })

    it('orders by word', () => {
      editor.setText(
        '1Hydrogen1  \n' +
        '1Beryllium1 \n' +
        '1Carbon1    \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '1Beryllium1 \n' +
          '1Carbon1    \n' +
          '1Hydrogen1  \n'
        )
      )
    })

    it('orders by trailing numeral', () => {
      editor.setText(
        'a4  \n' +
        'a0  \n' +
        'a12 \n' +
        'a1  \n' +
        'a2  \n' +
        'a3  \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          'a0  \n' +
          'a1  \n' +
          'a2  \n' +
          'a3  \n' +
          'a4  \n' +
          'a12 \n'
        )
      )
    })

    it('orders by leading numeral before word', () => {
      editor.setText(
        '4b \n' +
        '3a \n' +
        '2b \n' +
        '1a \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '1a \n' +
          '2b \n' +
          '3a \n' +
          '4b \n'
        )
      )
    })

    it('orders by word before trailing number', () => {
      editor.setText(
        'c2 \n' +
        'a4 \n' +
        'd1 \n' +
        'b3 \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          'a4 \n' +
          'b3 \n' +
          'c2 \n' +
          'd1 \n'
        )
      )
    })

    it('properly handles leading zeros', () => {
      editor.setText(
        'a01  \n' +
        'a001 \n' +
        'a003 \n' +
        'a002 \n' +
        'a02  \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          'a001 \n' +
          'a002 \n' +
          'a003 \n' +
          'a01  \n' +
          'a02  \n'
        )
      )
    })

    it('properly handles simple numerics', () => {
      editor.setText(
        '10 \n' +
        '9  \n' +
        '2  \n' +
        '1  \n' +
        '4  \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '1  \n' +
          '2  \n' +
          '4  \n' +
          '9  \n' +
          '10 \n'
        )
      )
    })

    it('properly handles floats', () => {
      editor.setText(
        '10.0401   \n' +
        '10.022    \n' +
        '10.042    \n' +
        '10.021999 \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '10.021999 \n' +
          '10.022    \n' +
          '10.0401   \n' +
          '10.042    \n'
        )
      )
    })

    it('properly handles float & decimal notation', () => {
      editor.setText(
        '10.04f  \n' +
        '10.039F \n' +
        '10.038d \n' +
        '10.037D \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
        '10.037D \n' +
        '10.038d \n' +
        '10.039F \n' +
        '10.04f  \n'
        )
      )
    })

    it('properly handles scientific notation', () => {
      editor.setText(
        '1.528535048e5 \n' +
        '1.528535047e7 \n' +
        '1.528535049e3 \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '1.528535049e3 \n' +
          '1.528535048e5 \n' +
          '1.528535047e7 \n'
        )
      )
    })

    it('properly handles ip addresses', () => {
      editor.setText(
        '192.168.0.100 \n' +
        '192.168.0.1   \n' +
        '192.168.1.1   \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '192.168.0.1   \n' +
          '192.168.0.100 \n' +
          '192.168.1.1   \n'
        )
      )
    })

    it('properly handles filenames', () => {
      editor.setText(
        'car.mov             \n' +
        '01alpha.sgi         \n' +
        '001alpha.sgi        \n' +
        'my.string_41299.tif \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '001alpha.sgi        \n' +
          '01alpha.sgi         \n' +
          'car.mov             \n' +
          'my.string_41299.tif \n'
        )
      )
    })

    it('properly handles dates', () => {
      editor.setText(
        '10/12/2008 \n' +
        '10/11/2008 \n' +
        '10/11/2007 \n' +
        '10/12/2007 \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '10/11/2007 \n' +
          '10/12/2007 \n' +
          '10/11/2008 \n' +
          '10/12/2008 \n'
        )
      )
    })

    it('properly handles money', () => {
      editor.setText(
        '$10002.00 \n' +
        '$10001.02 \n' +
        '$10001.01 \n'
      )

      sortLinesNatural(() =>
        expect(editor.getText()).toBe(
          '$10001.01 \n' +
          '$10001.02 \n' +
          '$10002.00 \n'
        )
      )
    })
  })
})
<|endoftext|>


<commit_before><commit_msg>Update project jquery.finger to 0.1.4<commit_after><|endoftext|>


<commit_before>// ==UserScript==
// @name         Godville Timer
// @version      1.2
// @description  Helps determine where the minute mark changes in Godville and send your godvoice right before it.
// @author       Koviko <koviko.net@gmail.com>
// @website      http://koviko.net/
// @match        http://godvillegame.com/superhero
// @grant        none
// ==/UserScript==

(function ($) {
	"use strict";

	var lastTimeSelector = "#diary .d_time:first",
		containerTemplate = "

:--

:

:--

",
		queueTemplate = "

Not Queued<button id=queue-voice class=not-queued>Queue Voice</button>

Queued<button id=dequeue-voice class=queued>Dequeue Voice</button>

",
		godvoiceButtonSelector = "#voice_submit_wrap input",
		queueButtonSelector = "#queue-voice",
		dequeueButtonSelector = "#dequeue-voice",
		earliestSecondSelector = "#earliest-second span",
		latestSecondSelector = "#latest-second span",
		currentSecondContainerSelector = "#current-second",
		currentSecondSelector = "#current-second span",
		//godInteractionSelector = ".m_infl",
		timeStates = { before: "before", during: "during", exact: "exact", after: "" },
		defaultValue = "--",
		queueClassName = "active",
		readyClassName = "is-ready",
		storageKey = "godville-timer",
		timeDelimiter = ":",
		halfDay = 12 * 60 * 60 * 1000,
		maxTimeDifference = 59 * 1000,
		secondsPerMinute = 60,
		millisecondsPerMinute = secondsPerMinute * 1000,
		maxLength = 100,
		maxDisplayDifference = 10,
		beforeThreshold = 3,
		exactOffset = -1,
		pad = function (value) { var abs = Math.abs(value); return (value < 0 ? "-" : "") + (abs < 10 ? "0" : "") + abs; },
		toSeconds = function (value) { value = value % secondsPerMinute; return value < 0 ? value + secondsPerMinute : value; };

	$(function () {
		var lastTime = null,
			lastUpdate = null,
			allSeconds = JSON.parse(localStorage.getItem(storageKey)) || [],
			container = $(containerTemplate).prependTo(document.body),
			queue = $(queueTemplate).appendTo(container),
			queueButton = $(queueButtonSelector, queue),
			dequeueButton = $(dequeueButtonSelector, queue),
			godvoiceButton = null,
			earliestSecondElement = $(earliestSecondSelector, container),
			latestSecondElement = $(latestSecondSelector, container),
			currentSecondContainer = $(currentSecondContainerSelector, container),
			currentSecondElement = $(currentSecondSelector, container),
			currentSecond = (new Date()).getSeconds(),
			currentState = null,
			isFirst = true,
			isQueued = false,
			isReady = false,
			sortTime,
			onlyUnique,
			updateQueue,
			changeState,
			updateDisplay,
			validateTime,
			addTime,
			checkTime;

		sortTime = function (a, b) { // Sort the time elements
			return a.second - b.second;
		};

		onlyUnique = function (value, index, self) { // Filter non-unique values
			return self.indexOf(value) === index;
		};

		updateQueue = function (isQ) { // Update state of queue
			isQueued = isQ;
			queue.toggleClass(queueClassName, isQ);
		};

		changeState = function (newState) { // Update state of current second
			if (newState !== currentState) {
				// Change the class
				currentSecondContainer.removeClass(currentState);
				currentSecondContainer.addClass(newState);

				// Update state
				currentState = newState;

				// React to "exact" state
				if (currentState === timeStates.exact && isQueued) {
					updateQueue(false);

					// Make sure the button exists
					if (!godvoiceButton || !godvoiceButton.length) {
						godvoiceButton = $(godvoiceButtonSelector);
					}

					// Click the button
					if (!godvoiceButton.is(":disabled")) {
						godvoiceButton.click();
					}
				}
			}
		};

		updateDisplay = function () { // Update the display
			var earliestSecond = null,
				latestSecond = null,
				beforeSecond = null,
				difference = null,
				normalizedDifference = null,
				beforeDifference = null,
				exactSecond = null,
				state = null;

			if (allSeconds.length > 1) {
				// Determine the earliest and latest seconds
				earliestSecond = toSeconds(allSeconds[0].second);
				latestSecond = toSeconds(allSeconds[allSeconds.length - 1].second);
				beforeSecond = toSeconds(latestSecond - beforeThreshold);
				difference = earliestSecond - latestSecond;
				normalizedDifference = toSeconds(difference);
				beforeDifference = latestSecond - beforeSecond;
				exactSecond = earliestSecond + exactOffset;

				// Determine if the current second is in between the latest and earliest seconds
				if (normalizedDifference <= maxDisplayDifference && normalizedDifference < 0) {
					// Update the ready state
					if (!isReady) {
						isReady = true;
						container.addClass(readyClassName);
					}

					// Update the second display
					if (currentSecond === exactSecond) {
						state = timeStates.exact;
					} else if (difference >= 0 && (currentSecond < earliestSecond && currentSecond >= latestSecond)) {
						state = timeStates.during;
					} else if (difference < 0 && (currentSecond < earliestSecond || currentSecond >= latestSecond)) {
						state = timeStates.during;
					} else if (beforeDifference >= 0 && (currentSecond < latestSecond && currentSecond >= beforeSecond)) {
						state = timeStates.before;
					} else if (beforeDifference < 0 && (currentSecond < latestSecond || currentSecond >= beforeSecond)) {
						state = timeStates.before;
					} else {
						state = timeStates.after;
					}
				} else if (isReady) {
					isReady = false;
					container.removeClass(readyClassName);
				}

				// Update the earliest & latest seconds
				earliestSecondElement.text(pad(earliestSecond));
				latestSecondElement.text(pad(latestSecond));
			} else {
				// Clear the earliest and latest seconds
				earliestSecondElement.text(defaultValue);
				latestSecondElement.text(defaultValue);
			}

			// Update the current second display
			currentSecondElement.text(pad(currentSecond));

			// Update the display's state
			changeState(state);
		};

		validateTime = function (timeObject) { // Add a new time and validate existing times against it
			// Add new time object
			if (timeObject) {
				allSeconds.push(timeObject);
			} else if (!allSeconds.length) {
				return;
			}

			// Sort all times
			allSeconds.sort(sortTime);

			// Validate earliest and latest seconds are within 60 seconds of each other
			while ((allSeconds[allSeconds.length - 1].second - allSeconds[0].second) >= secondsPerMinute) {
				// Remove first and last element
				allSeconds.pop();
				allSeconds.shift();
			}

			// Save to localStorage
			if (allSeconds.length > maxLength) {
				allSeconds = allSeconds.filter(onlyUnique);
			}

			localStorage.setItem(storageKey, JSON.stringify(allSeconds));
		};

		addTime = function (seconds, time) { // Add a new time
			if (isFirst) {
				isFirst = false;
				return;
			}

			// Add to collection and validate existing elements
			validateTime({ second: seconds, time: time });
		};

		checkTime = function () { // Check if the time has been updated
			var element = $(lastTimeSelector),
				time = element.text(),
				timeParts = time.split(timeDelimiter),
				then = new Date(),
				now = new Date(),
				timeDifference = 0,
				timestamp = now.getTime(),
				realSeconds = null,
				sinceLastUpdate = null,
				//isGodUpdate = $(godInteractionSelector, element).length > 0,
				timeText;

			// Update the current second
			currentSecond = now.getSeconds();
			realSeconds = currentSecond;

			// Get the actual date of the time
			timeParts = time.split(timeDelimiter);
			then.setHours(parseInt(timeParts[0], 10));
			then.setMinutes(parseInt(timeParts[1], 10));

			// Determine the minute difference between the displayed time and the local time
			timeDifference = Math.floor(((timestamp % halfDay) - (then.getTime() % halfDay)) / millisecondsPerMinute);

			if (timeDifference !== 0) {
				realSeconds += timeDifference * secondsPerMinute;
			}

			// Determine the time difference since the last update
			if (lastUpdate) {
				sinceLastUpdate = Math.abs(timestamp - lastUpdate);
			}

			// Update this as the latest update
			lastUpdate = timestamp;

			// Determine if the time is new
			if (time !== lastTime) { //&& !isGodUpdate) {
				// Determine whether the current time can be considered valid
				if (sinceLastUpdate !== null && sinceLastUpdate < maxTimeDifference) {
					// Make the time readable
					timeText = (pad(now.getHours() % 12) || 12) + timeDelimiter + pad(now.getMinutes()) + timeDelimiter + pad(currentSecond);

					// Add the time to the collection
					addTime(realSeconds, timeText);
				}

				// Update last time
				lastTime = time;
			}

			// Update display
			updateDisplay();
		};

		// Initiate the display
		if (allSeconds.length) {
			updateDisplay();
		}

		// Add queue actions
		queueButton.click(function () { updateQueue(true); });
		dequeueButton.click(function () { updateQueue(false); });

		// Add styles
		$("head").append("");

		setInterval(checkTime, 500);
	});
}(window.jQuery));<commit_msg>Fixed typo in last release.<commit_after>// ==UserScript==
// @name         Godville Timer
// @version      1.3
// @description  Helps determine where the minute mark changes in Godville and send your godvoice right before it.
// @author       Koviko <koviko.net@gmail.com>
// @website      http://koviko.net/
// @match        http://godvillegame.com/superhero
// @grant        none
// ==/UserScript==

(function ($) {
	"use strict";

	var lastTimeSelector = "#diary .d_time:first",
		containerTemplate = "

:--

:

:--

",
		queueTemplate = "

Not Queued<button id=queue-voice class=not-queued>Queue Voice</button>

Queued<button id=dequeue-voice class=queued>Dequeue Voice</button>

",
		godvoiceButtonSelector = "#voice_submit_wrap input",
		queueButtonSelector = "#queue-voice",
		dequeueButtonSelector = "#dequeue-voice",
		earliestSecondSelector = "#earliest-second span",
		latestSecondSelector = "#latest-second span",
		currentSecondContainerSelector = "#current-second",
		currentSecondSelector = "#current-second span",
		//godInteractionSelector = ".m_infl",
		timeStates = { before: "before", during: "during", exact: "exact", after: "" },
		defaultValue = "--",
		queueClassName = "active",
		readyClassName = "is-ready",
		storageKey = "godville-timer",
		timeDelimiter = ":",
		halfDay = 12 * 60 * 60 * 1000,
		maxTimeDifference = 59 * 1000,
		secondsPerMinute = 60,
		millisecondsPerMinute = secondsPerMinute * 1000,
		maxLength = 100,
		maxDisplayDifference = 10,
		beforeThreshold = 3,
		exactOffset = -1,
		pad = function (value) { var abs = Math.abs(value); return (value < 0 ? "-" : "") + (abs < 10 ? "0" : "") + abs; },
		toSeconds = function (value) { value = value % secondsPerMinute; return value < 0 ? value + secondsPerMinute : value; };

	$(function () {
		var lastTime = null,
			lastUpdate = null,
			allSeconds = JSON.parse(localStorage.getItem(storageKey)) || [],
			container = $(containerTemplate).prependTo(document.body),
			queue = $(queueTemplate).appendTo(container),
			queueButton = $(queueButtonSelector, queue),
			dequeueButton = $(dequeueButtonSelector, queue),
			godvoiceButton = null,
			earliestSecondElement = $(earliestSecondSelector, container),
			latestSecondElement = $(latestSecondSelector, container),
			currentSecondContainer = $(currentSecondContainerSelector, container),
			currentSecondElement = $(currentSecondSelector, container),
			currentSecond = (new Date()).getSeconds(),
			currentState = null,
			isFirst = true,
			isQueued = false,
			isReady = false,
			sortTime,
			onlyUnique,
			updateQueue,
			changeState,
			updateDisplay,
			validateTime,
			addTime,
			checkTime;

		sortTime = function (a, b) { // Sort the time elements
			return a.second - b.second;
		};

		onlyUnique = function (value, index, self) { // Filter non-unique values
			return self.indexOf(value) === index;
		};

		updateQueue = function (isQ) { // Update state of queue
			isQueued = isQ;
			queue.toggleClass(queueClassName, isQ);
		};

		changeState = function (newState) { // Update state of current second
			if (newState !== currentState) {
				// Change the class
				currentSecondContainer.removeClass(currentState);
				currentSecondContainer.addClass(newState);

				// Update state
				currentState = newState;

				// React to "exact" state
				if (currentState === timeStates.exact && isQueued) {
					updateQueue(false);

					// Make sure the button exists
					if (!godvoiceButton || !godvoiceButton.length) {
						godvoiceButton = $(godvoiceButtonSelector);
					}

					// Click the button
					if (!godvoiceButton.is(":disabled")) {
						godvoiceButton.click();
					}
				}
			}
		};

		updateDisplay = function () { // Update the display
			var earliestSecond = null,
				latestSecond = null,
				beforeSecond = null,
				difference = null,
				normalizedDifference = null,
				beforeDifference = null,
				exactSecond = null,
				state = null;

			if (allSeconds.length > 1) {
				// Determine the earliest and latest seconds
				earliestSecond = toSeconds(allSeconds[0].second);
				latestSecond = toSeconds(allSeconds[allSeconds.length - 1].second);
				beforeSecond = toSeconds(latestSecond - beforeThreshold);
				difference = earliestSecond - latestSecond;
				normalizedDifference = toSeconds(difference);
				beforeDifference = latestSecond - beforeSecond;
				exactSecond = earliestSecond + exactOffset;

				// Determine if the current second is in between the latest and earliest seconds
				if (normalizedDifference <= maxDisplayDifference && normalizedDifference > 0) {
					// Update the ready state
					if (!isReady) {
						isReady = true;
						container.addClass(readyClassName);
					}

					// Update the second display
					if (currentSecond === exactSecond) {
						state = timeStates.exact;
					} else if (difference >= 0 && (currentSecond < earliestSecond && currentSecond >= latestSecond)) {
						state = timeStates.during;
					} else if (difference < 0 && (currentSecond < earliestSecond || currentSecond >= latestSecond)) {
						state = timeStates.during;
					} else if (beforeDifference >= 0 && (currentSecond < latestSecond && currentSecond >= beforeSecond)) {
						state = timeStates.before;
					} else if (beforeDifference < 0 && (currentSecond < latestSecond || currentSecond >= beforeSecond)) {
						state = timeStates.before;
					} else {
						state = timeStates.after;
					}
				} else if (isReady) {
					isReady = false;
					container.removeClass(readyClassName);
				}

				// Update the earliest & latest seconds
				earliestSecondElement.text(pad(earliestSecond));
				latestSecondElement.text(pad(latestSecond));
			} else {
				// Clear the earliest and latest seconds
				earliestSecondElement.text(defaultValue);
				latestSecondElement.text(defaultValue);
			}

			// Update the current second display
			currentSecondElement.text(pad(currentSecond));

			// Update the display's state
			changeState(state);
		};

		validateTime = function (timeObject) { // Add a new time and validate existing times against it
			// Add new time object
			if (timeObject) {
				allSeconds.push(timeObject);
			} else if (!allSeconds.length) {
				return;
			}

			// Sort all times
			allSeconds.sort(sortTime);

			// Validate earliest and latest seconds are within 60 seconds of each other
			while ((allSeconds[allSeconds.length - 1].second - allSeconds[0].second) >= secondsPerMinute) {
				// Remove first and last element
				allSeconds.pop();
				allSeconds.shift();
			}

			// Save to localStorage
			if (allSeconds.length > maxLength) {
				allSeconds = allSeconds.filter(onlyUnique);
			}

			localStorage.setItem(storageKey, JSON.stringify(allSeconds));
		};

		addTime = function (seconds, time) { // Add a new time
			if (isFirst) {
				isFirst = false;
				return;
			}

			// Add to collection and validate existing elements
			validateTime({ second: seconds, time: time });
		};

		checkTime = function () { // Check if the time has been updated
			var element = $(lastTimeSelector),
				time = element.text(),
				timeParts = time.split(timeDelimiter),
				then = new Date(),
				now = new Date(),
				timeDifference = 0,
				timestamp = now.getTime(),
				realSeconds = null,
				sinceLastUpdate = null,
				//isGodUpdate = $(godInteractionSelector, element).length > 0,
				timeText;

			// Update the current second
			currentSecond = now.getSeconds();
			realSeconds = currentSecond;

			// Get the actual date of the time
			timeParts = time.split(timeDelimiter);
			then.setHours(parseInt(timeParts[0], 10));
			then.setMinutes(parseInt(timeParts[1], 10));

			// Determine the minute difference between the displayed time and the local time
			timeDifference = Math.floor(((timestamp % halfDay) - (then.getTime() % halfDay)) / millisecondsPerMinute);

			if (timeDifference !== 0) {
				realSeconds += timeDifference * secondsPerMinute;
			}

			// Determine the time difference since the last update
			if (lastUpdate) {
				sinceLastUpdate = Math.abs(timestamp - lastUpdate);
			}

			// Update this as the latest update
			lastUpdate = timestamp;

			// Determine if the time is new
			if (time !== lastTime) { //&& !isGodUpdate) {
				// Determine whether the current time can be considered valid
				if (sinceLastUpdate !== null && sinceLastUpdate < maxTimeDifference) {
					// Make the time readable
					timeText = (pad(now.getHours() % 12) || 12) + timeDelimiter + pad(now.getMinutes()) + timeDelimiter + pad(currentSecond);

					// Add the time to the collection
					addTime(realSeconds, timeText);
				}

				// Update last time
				lastTime = time;
			}

			// Update display
			updateDisplay();
		};

		// Initiate the display
		if (allSeconds.length) {
			updateDisplay();
		}

		// Add queue actions
		queueButton.click(function () { updateQueue(true); });
		dequeueButton.click(function () { updateQueue(false); });

		// Add styles
		$("head").append("");

		setInterval(checkTime, 500);
	});
}(window.jQuery));<|endoftext|>


<commit_before>var keystone = require('../../'),
	_ = require('underscore'),
	async = require('async'),
	moment = require('moment'),
	csv = require('csv');

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;

exports = module.exports = function(req, res) {

	var filters = req.list.processFilters(req.query.q),
		queryFilters = req.list.getSearchFilters(req.query.search, filters);

	var relFields = [];
	_.each(req.list.fields, function(field) {
		if (field.type === 'relationship') {
			relFields.push(field.path);
		}
	});

	var getRowData = function getRowData(i) {

		var rowData = { id: i.id };

		if (req.list.get('autokey')) {
			rowData[req.list.get('autokey').path] = i.get(req.list.get('autokey').path);
		}

		_.each(req.list.fields, function(field) {
			if (field.type === 'boolean') {
				rowData[field.path] = i.get(field.path) ? 'true' : 'false';
			} else if(field.type === 'relationship') {
                if(field.many){
                    var refData = i.get(field.path);
                    var values = [];
                    if (Array.isArray(refData) && refData.length) {
                        _.forEach(refData, function(i) {
                            values.push(field.refList.getDocumentName(i));
                        });
                    }
                    rowData[field.path] = values.join(', ');
                }else {
                    rowData[field.path] = field.refList.getDocumentName(i);
                }
			} else {
				rowData[field.path] = field.format(i);
			}
		});

		return rowData;

	};

	var query = req.list.model.find(queryFilters);
    if(relFields) {
        query.populate(relFields.join(' '))
    }
    query.exec(function(err, results) {

		var sendCSV = function(data) {

			var columns = data.length ? Object.keys(data[0]) : [];

			csv().from(data).to(res.attachment(req.list.path + '-' + moment().format('YYYYMMDD-HHMMSS') + '.csv'), {
				header: true,
				columns: columns
			});

		};

		if (!results.length) {
			// fast bail on no results
			return sendCSV([]);
		}
		var data;

		if (results[0].toCSV) {

			/**
			 * Custom toCSV Method present
			 *
			 * Detect dependencies and call it. If the last dependency is `callback`, call it asynchronously.
			 *
			 * Support dependencies are:
			 *   - req (current express request object)
			 *   - user (currently authenticated user)
			 *   - row (default row data, as generated without custom toCSV())
			 *   - callback (invokes async mode, must be provided last)
			 */

			var deps = _.map(results[0].toCSV.toString().match(FN_ARGS)[1].split(','), function(i) { return i.trim(); });

			var includeRowData = (deps.indexOf('row') > -1);

			var map = {
				req: req,
				user: req.user
			};

			var applyDeps = function(fn, _this, _map) {
				var args = _.map(deps, function(key) {
					return _map[key];
				});
				return fn.apply(_this, args);
			};

			if (_.last(deps) === 'callback') {
				// Allow async toCSV by detecting the last argument is callback
				return async.map(results, function(i, callback) {
					var _map = _.clone(map);
					_map.callback = callback;
					if (includeRowData) {
						_map.row = getRowData(i);
					}
					applyDeps(i.toCSV, i, _map);
				}, function(err, results) {
					if (err) {
						console.log('Error generating CSV for list ' + req.list.key);
						console.log(err);
						return res.send(keystone.wrapHTMLError('Error generating CSV', 'Please check the log for more details, or contact support.'));
					}
					sendCSV(results);
				});
			} else {
				// Without a callback, toCSV must return the value
				data = [];
				if (includeRowData) {
					// if row data is required, add it to the map for each iteration
				_.each(results, function(i) {
						var _map = _.clone(map);
						_map.row = getRowData(i);
						data.push(applyDeps(i.toCSV, i, _map));
					});
				} else {
					// fast path: use the same map for each iteration
					_.each(results, function(i) {
					data.push(applyDeps(i.toCSV, i, map));
				});
				}
				return sendCSV(data);
			}

		} else {

			/**
			 * Generic conversion to CSV
			 *
			 * Loops through each of the fields in the List and uses each field's `format` method
			 * to generate the data
			 */

			data = [];
			_.each(results, function(i) {
				data.push(getRowData(i));
			});
			return sendCSV(data);
		}

	});

};
<commit_msg>user refData for single relationship<commit_after>var keystone = require('../../'),
	_ = require('underscore'),
	async = require('async'),
	moment = require('moment'),
	csv = require('csv');

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;

exports = module.exports = function(req, res) {

	var filters = req.list.processFilters(req.query.q),
		queryFilters = req.list.getSearchFilters(req.query.search, filters);

	var relFields = [];
	_.each(req.list.fields, function(field) {
		if (field.type === 'relationship') {
			relFields.push(field.path);
		}
	});

	var getRowData = function getRowData(i) {

		var rowData = { id: i.id };

		if (req.list.get('autokey')) {
			rowData[req.list.get('autokey').path] = i.get(req.list.get('autokey').path);
		}

		_.each(req.list.fields, function(field) {
			if (field.type === 'boolean') {
				rowData[field.path] = i.get(field.path) ? 'true' : 'false';
			} else if(field.type === 'relationship') {
                var refData = i.get(field.path);
                if(field.many){
                    var values = [];
                    if (Array.isArray(refData) && refData.length) {
                        _.forEach(refData, function(i) {
                            values.push(field.refList.getDocumentName(i));
                        });
                    }
                    rowData[field.path] = values.join(', ');
                }else {
                    rowData[field.path] = field.refList.getDocumentName(refData);
                }
			} else {
				rowData[field.path] = field.format(i);
			}
		});

		return rowData;

	};

	var query = req.list.model.find(queryFilters);
    if(relFields) {
        query.populate(relFields.join(' '))
    }
    query.exec(function(err, results) {

		var sendCSV = function(data) {

			var columns = data.length ? Object.keys(data[0]) : [];

			csv().from(data).to(res.attachment(req.list.path + '-' + moment().format('YYYYMMDD-HHMMSS') + '.csv'), {
				header: true,
				columns: columns
			});

		};

		if (!results.length) {
			// fast bail on no results
			return sendCSV([]);
		}
		var data;

		if (results[0].toCSV) {

			/**
			 * Custom toCSV Method present
			 *
			 * Detect dependencies and call it. If the last dependency is `callback`, call it asynchronously.
			 *
			 * Support dependencies are:
			 *   - req (current express request object)
			 *   - user (currently authenticated user)
			 *   - row (default row data, as generated without custom toCSV())
			 *   - callback (invokes async mode, must be provided last)
			 */

			var deps = _.map(results[0].toCSV.toString().match(FN_ARGS)[1].split(','), function(i) { return i.trim(); });

			var includeRowData = (deps.indexOf('row') > -1);

			var map = {
				req: req,
				user: req.user
			};

			var applyDeps = function(fn, _this, _map) {
				var args = _.map(deps, function(key) {
					return _map[key];
				});
				return fn.apply(_this, args);
			};

			if (_.last(deps) === 'callback') {
				// Allow async toCSV by detecting the last argument is callback
				return async.map(results, function(i, callback) {
					var _map = _.clone(map);
					_map.callback = callback;
					if (includeRowData) {
						_map.row = getRowData(i);
					}
					applyDeps(i.toCSV, i, _map);
				}, function(err, results) {
					if (err) {
						console.log('Error generating CSV for list ' + req.list.key);
						console.log(err);
						return res.send(keystone.wrapHTMLError('Error generating CSV', 'Please check the log for more details, or contact support.'));
					}
					sendCSV(results);
				});
			} else {
				// Without a callback, toCSV must return the value
				data = [];
				if (includeRowData) {
					// if row data is required, add it to the map for each iteration
				_.each(results, function(i) {
						var _map = _.clone(map);
						_map.row = getRowData(i);
						data.push(applyDeps(i.toCSV, i, _map));
					});
				} else {
					// fast path: use the same map for each iteration
					_.each(results, function(i) {
					data.push(applyDeps(i.toCSV, i, map));
				});
				}
				return sendCSV(data);
			}

		} else {

			/**
			 * Generic conversion to CSV
			 *
			 * Loops through each of the fields in the List and uses each field's `format` method
			 * to generate the data
			 */

			data = [];
			_.each(results, function(i) {
				data.push(getRowData(i));
			});
			return sendCSV(data);
		}

	});

};
<|endoftext|>


<commit_before>/*
 * genscripts.js - ilib tool to generate the json data about ISO 15924 scripts
 *
 * Copyright © 2013 - 2017, 2020 JEDLSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * This code is intended to be run under node.js
 */

var fs = require('fs');
var unifile = require('./unifile.js');
var common = require('./common.js');
var UnicodeFile = unifile.UnicodeFile;
var coelesce = common.coelesce;

function usage() {
	console.log("Usage: genscripts [-h] ISO-15924-file.txt ScriptMetadata.txt UCD-dir  [toDir]\n" +
			"Generate the normalization data.\n\n" +
			"-h or --help\n" +
			"  this help\n" +
			"ISO-15924-file.txt\n" +
			"  the Unicode script code definition file downloaded from the Unicode site at\n" +
			"  https://unicode.org/iso15924/iso15924-text.html\n" +
			"scriptMetadata.txt\n" +
			"  path to the scriptMetadata.txt file downloaded from the Unicode site\n" +
			"  i.e) http://unicode.org/Public/cldr/36/ --> common/properties/scriptMetadata.txt\n" +
			"UCD-dir\n" +
			"  path to the Unicode Character Database files downloaded from the Unicode site\n" +
			"  i.e) https://unicode.org/Public/13.0.0/ucd/ \n" +
			"toDir\n" +
			"  directory to output the normalization json files. Default: current dir.\n");
	process.exit(1);
}

var iso15924FileName;
var scriptFileName;
var scriptMetaDataFileName;
var ucdDir;
var toDir = ".";

process.argv.forEach(function (val, index, array) {
	if (val === "-h" || val === "--help") {
		usage();
	}
});

if (process.argv.length < 4) {
	console.error('Error: not enough arguments');
	usage();
}

iso15924FileName = process.argv[2];
scriptMetaDataFileName = process.argv[3];
ucdDir = process.argv[4];

if (process.argv.length > 5) {
	toDir = process.argv[5];
}

console.log("genscripts - generate scripts data.\n" +
		"Copyright (c) 2012 - 2016 JEDLSoft");

// TODO: these should call fs.existsSync instead
fs.exists(iso15924FileName, function (exists) {
	if (!exists) {
		console.error("Could not access file " + iso15924FileName);
		usage();
	}
});

fs.exists(ucdDir, function (exists) {
	if (!exists) {
		console.error("Could not access UCD directory " + ucdDir);
		usage();
	}
});

fs.exists(scriptMetaDataFileName, function (exists) {
	if (!exists) {
		console.error("Could not access file " + scriptMetaDataFileName);
		usage();
	}
});

fs.exists(toDir, function (exists) {
	if (!exists) {
		console.error("Could not access target directory " + toDir);
		usage();
	}
});

scriptFileName = ucdDir + "/Scripts.txt";

fs.exists(scriptFileName, function (exists) {
	if (!exists) {
		console.error("Could not access file " + scriptFileName);
		usage();
	}
});

var scripts = {};
var fullToShortMap = {};

var iso15924 = new UnicodeFile({path: iso15924FileName});
var len = iso15924.length();
var row;
var script;
var longCode;
var code;

for (var i = 0; i < len; i++ ) {
	row = iso15924.get(i);
	code = row[0];

	longCode = (row[4].length == 0) ? row[2] : row[4];
	longCode = longCode.replace(/ +/g, '_');
	script = {
		nb: parseInt(row[1], 10),
		nm: row[2],
		lid: longCode
	};

	scripts[row[0]] = script;
	if (longCode.length > 0) {
		fullToShortMap[longCode.toLowerCase()] = row[0];
	}
}

var scriptMetaData = new UnicodeFile({path: scriptMetaDataFileName});
var len = scriptMetaData.length();

for (var i = 0; i < len; i++ ) {
	row = scriptMetaData.get(i);
	code = row[0];
	console.log(code + " isrtl " + row[6]);

	// is this script written right-to-left?
	scripts[code].rtl = (row[6] === " YES" ? true : false);

	// does this script require an IME to enter text?
	scripts[code].ime = (row[9] === " YES" ? true : false);

	// does this script have the concept of upper- and lower-case?
	scripts[code].casing = (row[10] === " YES" ? true : false);
}

// the Unicode data has only the binary decompositions. That is, the first of
// two chars of a decomposition may be itself decomposable. So, apply the
// decompositions recursively here to pre-calculate the full decomposition
// before writing out the files.

fs.writeFile(toDir + "/scripts.json", JSON.stringify(scripts, true, 4), function (err) {
	if (err) {
		throw err;
	}
});

var scriptsFile = new UnicodeFile({path: scriptFileName});
var row, scriptName, rangeStart, rangeEnd, range;
var scriptToRange = {};
var ranges = [];
var rangeToScript = [];

for (var i = 0; i < scriptsFile.length(); i++) {
	row = scriptsFile.get(i);
	scriptName = row[1].trim();
	scriptName = fullToShortMap[scriptName.toLowerCase()] || scriptName;
	rangeStart = parseInt(row[0].match(/^[A-F0-9]+/)[0],16);
	rangeEnd = row[0].match(/\.\.[A-F0-9]+/);

	if (rangeEnd && rangeEnd.length > 0) {
		rangeEnd = parseInt(rangeEnd[0].substring(2), 16);
		range = [rangeStart, rangeEnd];
		ranges.push([scriptName, rangeStart, rangeEnd]);
	} else {
		range = [rangeStart];
		ranges.push([scriptName, rangeStart]);
	}

	if (!scriptToRange[scriptName]) {
		scriptToRange[scriptName] = [range];
	} else {
		scriptToRange[scriptName].push(range);
	}
}

for (script in scriptToRange) {
	if (script && scriptToRange[script]) {
		scriptToRange[script] = coelesce(scriptToRange[script], 0);
	}
}

function compareByStart(left, right) {
	return left[1] - right[1];
}

ranges.sort(compareByStart);
rangeToScript = coelesce(ranges, 1);

fs.writeFile(toDir + "/scriptToRange.json", JSON.stringify(scriptToRange, true, 4), function (err) {
	if (err) {
		throw err;
	}
});

fs.writeFile(toDir + "/rangeToScript.json", JSON.stringify(rangeToScript, true, 4), function (err) {
	if (err) {
		throw err;
	}
});
<commit_msg>Update copyright console log<commit_after>/*
 * genscripts.js - ilib tool to generate the json data about ISO 15924 scripts
 *
 * Copyright © 2013 - 2017, 2020 JEDLSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * This code is intended to be run under node.js
 */

var fs = require('fs');
var unifile = require('./unifile.js');
var common = require('./common.js');
var UnicodeFile = unifile.UnicodeFile;
var coelesce = common.coelesce;

function usage() {
	console.log("Usage: genscripts [-h] ISO-15924-file.txt ScriptMetadata.txt UCD-dir  [toDir]\n" +
			"Generate the normalization data.\n\n" +
			"-h or --help\n" +
			"  this help\n" +
			"ISO-15924-file.txt\n" +
			"  the Unicode script code definition file downloaded from the Unicode site at\n" +
			"  https://unicode.org/iso15924/iso15924-text.html\n" +
			"scriptMetadata.txt\n" +
			"  path to the scriptMetadata.txt file downloaded from the Unicode site\n" +
			"  i.e) http://unicode.org/Public/cldr/36/ --> common/properties/scriptMetadata.txt\n" +
			"UCD-dir\n" +
			"  path to the Unicode Character Database files downloaded from the Unicode site\n" +
			"  i.e) https://unicode.org/Public/13.0.0/ucd/ \n" +
			"toDir\n" +
			"  directory to output the normalization json files. Default: current dir.\n");
	process.exit(1);
}

var iso15924FileName;
var scriptFileName;
var scriptMetaDataFileName;
var ucdDir;
var toDir = ".";

process.argv.forEach(function (val, index, array) {
	if (val === "-h" || val === "--help") {
		usage();
	}
});

if (process.argv.length < 4) {
	console.error('Error: not enough arguments');
	usage();
}

iso15924FileName = process.argv[2];
scriptMetaDataFileName = process.argv[3];
ucdDir = process.argv[4];

if (process.argv.length > 5) {
	toDir = process.argv[5];
}

console.log("genscripts - generate scripts data.\n" +
		"Copyright (c) 2012 - 2017, 2020 JEDLSoft");

// TODO: these should call fs.existsSync instead
fs.exists(iso15924FileName, function (exists) {
	if (!exists) {
		console.error("Could not access file " + iso15924FileName);
		usage();
	}
});

fs.exists(ucdDir, function (exists) {
	if (!exists) {
		console.error("Could not access UCD directory " + ucdDir);
		usage();
	}
});

fs.exists(scriptMetaDataFileName, function (exists) {
	if (!exists) {
		console.error("Could not access file " + scriptMetaDataFileName);
		usage();
	}
});

fs.exists(toDir, function (exists) {
	if (!exists) {
		console.error("Could not access target directory " + toDir);
		usage();
	}
});

scriptFileName = ucdDir + "/Scripts.txt";

fs.exists(scriptFileName, function (exists) {
	if (!exists) {
		console.error("Could not access file " + scriptFileName);
		usage();
	}
});

var scripts = {};
var fullToShortMap = {};

var iso15924 = new UnicodeFile({path: iso15924FileName});
var len = iso15924.length();
var row;
var script;
var longCode;
var code;

for (var i = 0; i < len; i++ ) {
	row = iso15924.get(i);
	code = row[0];

	longCode = (row[4].length == 0) ? row[2] : row[4];
	longCode = longCode.replace(/ +/g, '_');
	script = {
		nb: parseInt(row[1], 10),
		nm: row[2],
		lid: longCode
	};

	scripts[row[0]] = script;
	if (longCode.length > 0) {
		fullToShortMap[longCode.toLowerCase()] = row[0];
	}
}

var scriptMetaData = new UnicodeFile({path: scriptMetaDataFileName});
var len = scriptMetaData.length();

for (var i = 0; i < len; i++ ) {
	row = scriptMetaData.get(i);
	code = row[0];
	console.log(code + " isrtl " + row[6]);

	// is this script written right-to-left?
	scripts[code].rtl = (row[6] === " YES" ? true : false);

	// does this script require an IME to enter text?
	scripts[code].ime = (row[9] === " YES" ? true : false);

	// does this script have the concept of upper- and lower-case?
	scripts[code].casing = (row[10] === " YES" ? true : false);
}

// the Unicode data has only the binary decompositions. That is, the first of
// two chars of a decomposition may be itself decomposable. So, apply the
// decompositions recursively here to pre-calculate the full decomposition
// before writing out the files.

fs.writeFile(toDir + "/scripts.json", JSON.stringify(scripts, true, 4), function (err) {
	if (err) {
		throw err;
	}
});

var scriptsFile = new UnicodeFile({path: scriptFileName});
var row, scriptName, rangeStart, rangeEnd, range;
var scriptToRange = {};
var ranges = [];
var rangeToScript = [];

for (var i = 0; i < scriptsFile.length(); i++) {
	row = scriptsFile.get(i);
	scriptName = row[1].trim();
	scriptName = fullToShortMap[scriptName.toLowerCase()] || scriptName;
	rangeStart = parseInt(row[0].match(/^[A-F0-9]+/)[0],16);
	rangeEnd = row[0].match(/\.\.[A-F0-9]+/);

	if (rangeEnd && rangeEnd.length > 0) {
		rangeEnd = parseInt(rangeEnd[0].substring(2), 16);
		range = [rangeStart, rangeEnd];
		ranges.push([scriptName, rangeStart, rangeEnd]);
	} else {
		range = [rangeStart];
		ranges.push([scriptName, rangeStart]);
	}

	if (!scriptToRange[scriptName]) {
		scriptToRange[scriptName] = [range];
	} else {
		scriptToRange[scriptName].push(range);
	}
}

for (script in scriptToRange) {
	if (script && scriptToRange[script]) {
		scriptToRange[script] = coelesce(scriptToRange[script], 0);
	}
}

function compareByStart(left, right) {
	return left[1] - right[1];
}

ranges.sort(compareByStart);
rangeToScript = coelesce(ranges, 1);

fs.writeFile(toDir + "/scriptToRange.json", JSON.stringify(scriptToRange, true, 4), function (err) {
	if (err) {
		throw err;
	}
});

fs.writeFile(toDir + "/rangeToScript.json", JSON.stringify(rangeToScript, true, 4), function (err) {
	if (err) {
		throw err;
	}
});
<|endoftext|>


<commit_before>import omit from 'lodash/omit'
import { List } from 'immutable'

/**
 * Commands.
 *
 * @type {Object}
 */

const Commands = {}

/**
 * Save an `operation` into the history.
 *
 * @param {Editor} editor
 * @param {Object} operation
 */

Commands.save = (editor, operation) => {
  const { operations, value } = editor
  const { data } = value
  let { save, merge } = editor.tmp
  if (save === false) return

  let undos = data.get('undos') || List()
  const lastBatch = undos.last()
  const lastOperation = lastBatch && lastBatch.last()

  // If `merge` is non-commital, and this is not the first operation in a new
  // editor, then merge, otherwise merge based on the last operation.
  if (merge == null) {
    if (operations.size !== 0) {
      merge = true
    } else {
      merge = shouldMerge(operation, lastOperation)
    }
  }

  // If the `merge` flag is true, add the operation to the last batch.
  if (merge && lastBatch) {
    const batch = lastBatch.push(operation)
    undos = undos.pop()
    undos = undos.push(batch)
  } else {
    // Otherwise, create a new batch with the operation.
    const batch = List([operation])
    undos = undos.push(batch)
  }

  // Constrain the history to 100 entries for memory's sake.
  if (undos.size > 100) {
    undos = undos.takeLast(100)
  }

  // Clear the redos and update the history.
  editor.withoutSaving(() => {
    const redos = List()
    const newData = data.set('undos', undos).set('redos', redos)
    editor.setData(newData)
  })
}

/**
 * Redo to the next value in the history.
 *
 * @param {Editor} editor
 */

Commands.redo = editor => {
  const { value } = editor
  const { data } = value
  let redos = data.get('redos') || List()
  let undos = data.get('undos') || List()
  const batch = redos.last()
  if (!batch) return

  editor.withoutSaving(() => {
    // Replay the batch of operations.
    batch.forEach(op => {
      const { type, properties } = op

      // When the operation mutates the selection, omit its `isFocused` value to
      // prevent the editor focus from changing during redoing.
      if (type === 'set_selection') {
        op = op.set('properties', omit(properties, 'isFocused'))
      }

      editor.applyOperation(op)
    })

    // Shift the next value into the undo stack.
    redos = redos.pop()
    undos = undos.push(batch)
    const newData = data.set('undos', undos).set('redos', redos)
    editor.setData(newData)
  })
}

/**
 * Undo the previous operations in the history.
 *
 * @param {Editor} editor
 */

Commands.undo = editor => {
  const { value } = editor
  const { data } = value
  let redos = data.get('redos') || List()
  let undos = data.get('undos') || List()
  const batch = undos.last()
  if (!batch) return

  editor.withoutSaving(() => {
    // Replay the inverse of the previous operations.
    batch
      .slice()
      .reverse()
      .map(op => op.invert())
      .forEach(inverse => {
        const { type, properties } = inverse

        // When the operation mutates the selection, omit its `isFocused` value to
        // prevent the editor focus from changing during undoing.
        if (type === 'set_selection') {
          inverse = inverse.set('properties', omit(properties, 'isFocused'))
        }

        editor.applyOperation(inverse)
      })

    // Shift the previous operations into the redo stack.
    redos = redos.push(batch)
    undos = undos.pop()
    const newData = data.set('undos', undos).set('redos', redos)
    editor.setData(newData)
  })
}

/**
 * Apply a series of changes inside a synchronous `fn`, without merging any of
 * the new operations into previous save point in the history.
 *
 * @param {Editor} editor
 * @param {Function} fn
 */

Commands.withoutMerging = (editor, fn) => {
  const value = editor.tmp.merge
  editor.tmp.merge = false
  fn(editor)
  editor.tmp.merge = value
}

/**
 * Apply a series of changes inside a synchronous `fn`, without saving any of
 * their operations into the history.
 *
 * @param {Editor} editor
 * @param {Function} fn
 */

Commands.withoutSaving = (editor, fn) => {
  const value = editor.tmp.save
  editor.tmp.save = false
  fn(editor)
  editor.tmp.save = value
}

/**
 * Check whether to merge a new operation `o` into the previous operation `p`.
 *
 * @param {Object} o
 * @param {Object} p
 * @return {Boolean}
 */

function shouldMerge(o, p) {
  if (!p) return false

  const merge =
    (o.type === 'set_selection' && p.type === 'set_selection') ||
    (o.type === 'insert_text' &&
      p.type === 'insert_text' &&
      o.offset === p.offset + p.text.length &&
      o.path.equals(p.path)) ||
    (o.type === 'remove_text' &&
      p.type === 'remove_text' &&
      o.offset + o.text.length === p.offset &&
      o.path.equals(p.path))

  return merge
}

/**
 * Export.
 *
 * @type {Object}
 */

export default Commands
<commit_msg>undo and redo operations are now wrapped with withoutNormalizing call to prevent normalizing occuring in between application or reversal of batched operations (#2462)<commit_after>import omit from 'lodash/omit'
import { List } from 'immutable'

/**
 * Commands.
 *
 * @type {Object}
 */

const Commands = {}

/**
 * Save an `operation` into the history.
 *
 * @param {Editor} editor
 * @param {Object} operation
 */

Commands.save = (editor, operation) => {
  const { operations, value } = editor
  const { data } = value
  let { save, merge } = editor.tmp
  if (save === false) return

  let undos = data.get('undos') || List()
  const lastBatch = undos.last()
  const lastOperation = lastBatch && lastBatch.last()

  // If `merge` is non-commital, and this is not the first operation in a new
  // editor, then merge, otherwise merge based on the last operation.
  if (merge == null) {
    if (operations.size !== 0) {
      merge = true
    } else {
      merge = shouldMerge(operation, lastOperation)
    }
  }

  // If the `merge` flag is true, add the operation to the last batch.
  if (merge && lastBatch) {
    const batch = lastBatch.push(operation)
    undos = undos.pop()
    undos = undos.push(batch)
  } else {
    // Otherwise, create a new batch with the operation.
    const batch = List([operation])
    undos = undos.push(batch)
  }

  // Constrain the history to 100 entries for memory's sake.
  if (undos.size > 100) {
    undos = undos.takeLast(100)
  }

  // Clear the redos and update the history.
  editor.withoutSaving(() => {
    const redos = List()
    const newData = data.set('undos', undos).set('redos', redos)
    editor.setData(newData)
  })
}

/**
 * Redo to the next value in the history.
 *
 * @param {Editor} editor
 */

Commands.redo = editor => {
  const { value } = editor
  const { data } = value
  let redos = data.get('redos') || List()
  let undos = data.get('undos') || List()
  const batch = redos.last()
  if (!batch) return

  editor.withoutSaving(() => {
    editor.withoutNormalizing(() => {
      // Replay the batch of operations.
      batch.forEach(op => {
        const { type, properties } = op

        // When the operation mutates the selection, omit its `isFocused` value to
        // prevent the editor focus from changing during redoing.
        if (type === 'set_selection') {
          op = op.set('properties', omit(properties, 'isFocused'))
        }

        editor.applyOperation(op)
      })

      // Shift the next value into the undo stack.
      redos = redos.pop()
      undos = undos.push(batch)
      const newData = data.set('undos', undos).set('redos', redos)
      editor.setData(newData)
    })
  })
}

/**
 * Undo the previous operations in the history.
 *
 * @param {Editor} editor
 */

Commands.undo = editor => {
  const { value } = editor
  const { data } = value
  let redos = data.get('redos') || List()
  let undos = data.get('undos') || List()
  const batch = undos.last()
  if (!batch) return

  editor.withoutSaving(() => {
    editor.withoutNormalizing(() => {
      // Replay the inverse of the previous operations.
      batch
        .slice()
        .reverse()
        .map(op => op.invert())
        .forEach(inverse => {
          const { type, properties } = inverse

          // When the operation mutates the selection, omit its `isFocused` value to
          // prevent the editor focus from changing during undoing.
          if (type === 'set_selection') {
            inverse = inverse.set('properties', omit(properties, 'isFocused'))
          }

          editor.applyOperation(inverse)
        })

      // Shift the previous operations into the redo stack.
      redos = redos.push(batch)
      undos = undos.pop()
      const newData = data.set('undos', undos).set('redos', redos)
      editor.setData(newData)
    })
  })
}

/**
 * Apply a series of changes inside a synchronous `fn`, without merging any of
 * the new operations into previous save point in the history.
 *
 * @param {Editor} editor
 * @param {Function} fn
 */

Commands.withoutMerging = (editor, fn) => {
  const value = editor.tmp.merge
  editor.tmp.merge = false
  fn(editor)
  editor.tmp.merge = value
}

/**
 * Apply a series of changes inside a synchronous `fn`, without saving any of
 * their operations into the history.
 *
 * @param {Editor} editor
 * @param {Function} fn
 */

Commands.withoutSaving = (editor, fn) => {
  const value = editor.tmp.save
  editor.tmp.save = false
  fn(editor)
  editor.tmp.save = value
}

/**
 * Check whether to merge a new operation `o` into the previous operation `p`.
 *
 * @param {Object} o
 * @param {Object} p
 * @return {Boolean}
 */

function shouldMerge(o, p) {
  if (!p) return false

  const merge =
    (o.type === 'set_selection' && p.type === 'set_selection') ||
    (o.type === 'insert_text' &&
      p.type === 'insert_text' &&
      o.offset === p.offset + p.text.length &&
      o.path.equals(p.path)) ||
    (o.type === 'remove_text' &&
      p.type === 'remove_text' &&
      o.offset + o.text.length === p.offset &&
      o.path.equals(p.path))

  return merge
}

/**
 * Export.
 *
 * @type {Object}
 */

export default Commands
<|endoftext|>


<commit_before>import React, { PropTypes } from "react"

import { repository } from "../../../package.json"

const EditThisPage = (props) => {
  return (
    <a
      href={
        repository.replace(/\.git$/, "") +
        "/edit/next/"+
        "docs/content/" + // phenomic docs specificity
        props.filename
      }
    >
      { "Edit this page" }
    </a>
  )
}

EditThisPage.propTypes = {
  filename: PropTypes.string.isRequired,
}

export default EditThisPage
<commit_msg>Docs: fix "edit this page" link<commit_after>import React, { PropTypes } from "react"

import { repository } from "../../../package.json"

const EditThisPage = (props) => {
  return (
    <a
      href={
        repository.replace(/\.git$/, "") +
        "/edit/master/"+
        "docs/content/" + // phenomic docs specificity
        props.filename
      }
    >
      { "Edit this page" }
    </a>
  )
}

EditThisPage.propTypes = {
  filename: PropTypes.string.isRequired,
}

export default EditThisPage
<|endoftext|>