<commit_before>import React, { Fragment } from 'react'
import {
  Heading,
  Container,
  Card,
  Flex,
  Box,
  Text,
  LargeButton,
  Icon,
  Section,
  Link as A
} from '@hackclub/design-system'
import Helmet from 'react-helmet'
import Link from 'gatsby-link'
import Nav from 'components/Nav'
import Stat from 'components/Stat'
import {
  Triangle,
  Hexagon,
  Pentagon,
  Square,
  Circle,
  Line
} from 'components/Shapes'
import DonateForm from 'components/donate/DonateForm'
import Spent from 'components/donate/Spent'
import Footer from 'components/Footer'
import commaNumber from 'comma-number'
import donors from 'components/donate/donors.json'

const Header = Section.withComponent('header').extend`
  background: url('/pattern.svg');
  > div {
    display: grid;
    grid-gap: ${({ theme }) => theme.space[4]}px;
    ${({ theme }) => theme.mediaQueries.md} {
      grid-template-columns: 3fr 2fr;
    }
  }
`

const Row = Box.extend`
  text-align: left;
  ${({ theme }) => theme.mediaQueries.md} {
    display: grid;
    grid-gap: ${({ theme }) => theme.space[3]}px;
    grid-template-columns: ${({ reverse }) =>
      reverse ? '3fr 2fr' : '2fr 3fr'};
  }
`

const Financials = Box.extend`
  display: grid;
  border-radius: ${({ theme }) => theme.radius};
  overflow: hidden;

  ${({ theme }) => theme.mediaQueries.md} {
    grid-template-columns: 2fr 3fr;
  }
`

const Stats = Box.extend`
  div {
    width: 100%;
    display: block;
    text-align: left !important;
  }
  span:before {
    content: '$';
    font-size: ${({ theme }) => theme.fontSizes[4]}px;
    margin-left: -12px;
    vertical-align: super;
  }
  p {
    color: ${({ theme }) => theme.colors.red[1]};
  }
`

const Shapes = Box.extend`
  display: none;
  ${({ theme }) => theme.mediaQueries.md} {
    display: block;
    float: right;
    position: relative;
    z-index: -1;
    svg {
      position: absolute;
    }
  }
`

const WishShapes = Shapes.extend`
  svg {
    &:first-child {
      top: 0;
      right: 0;
      color: rgba(115, 45, 228, 0.75);
      z-index: 1;
    }
    &:last-child {
      top: 0;
      right: 4rem;
      color: rgba(45, 228, 207, 0.75);
    }
  }
`
const ContributionShapes = Shapes.extend`
  svg {
    right: 12rem;
    top: 1rem;
    color: ${({ theme }) => theme.colors.pink[4]};
  }
`
const DonorsShapes = Shapes.extend`
  svg:first-child {
    right: 8rem;
    top: 2rem;
    color: ${({ theme }) => theme.colors.orange[4]};
  }
  svg:last-child {
    right: 4rem;
    top: 4rem;
    color: ${({ theme }) => theme.hexa('blue.4', 0.75)};
  }
`

const headline = { f: [5, 6], mb: 3, style: { lineHeight: '1.125' } }
const subhline = { f: [3, 4], style: { lineHeight: '1.375' } }
const subtext = { f: [3, 4], style: { lineHeight: '1.5' } }

const contentContainer = {
  maxWidth: 64,
  w: 1,
  p: 3,
  color: 'black',
  style: { position: 'relative' }
}
const content = { maxWidth: 48, mx: 0, color: 'black' }

A.link = A.withComponent(Link)

const title = 'Donate to Hack Club'
const description =
  'Contribute today to empower the next generation and help start a coding club at every high school.'

const stats = {
  monthly: 6742.71,
  student: 3,
  club: 60
}

const DonorCard = ({ children }) => (
  <Card p={3} m={2} color="black">
    <Flex align="center" justify="center">
      {children}
    </Flex>
  </Card>
)

const DonorListing = ({ name, url }) => (
  <Box w={[1 / 2, 1 / 3]}>
    {url ? (

        <DonorCard>
          {name} <Icon name="open_in_new" color="gray.4" ml={1} size={16} />
        </DonorCard>

    ) : (
      <DonorCard>{name}</DonorCard>
    )}
  </Box>
)

export default () => (
  <Fragment>
    <Helmet
      title={title}
      meta={[
        { name: 'twitter:title', content: title },
        { name: 'description', content: description },
        { name: 'twitter:description', content: description },
        { property: 'og:title', content: title },
        { property: 'og:description', content: description },
        { property: 'og:url', content: 'https://hackclub.com/donate' }
      ]}
    />

      <Container {...contentContainer} align="left" py={[4]}>
        <Container maxWidth={36} mx={0}>
          <Heading.h1 color="primary" f={[3, 4]} caps>
            Donate to Hack Club
          </Heading.h1>
          <Heading.h2 my={3} {...headline}>
            We rely on people like you to bring coding to the world.
          </Heading.h2>
          <Text {...subhline}>
            Contribute today to empower the next generation. Help start a Hack
            Club at every high school.
          </Text>
          <Text mt={3} f={2} color="muted">
            Your contribution is tax-deductible.

            Hack Club is a 501(c)(3) non-profit with the EIN 81-2908499.
          </Text>
        </Container>
        <Card
          bg="snow"
          p={[3, 4]}
          mt={[0, -3, -4, -5]}
          style={{ overflow: 'hidden' }}
        >
          <DonateForm />
        </Card>
      </Container>

    <Container {...contentContainer}>
      <WishShapes mt={3}>
        <Triangle size={128} rotate={64} />
        <Circle size={128} />
      </WishShapes>
      <Container {...content}>
        <Heading.h2 {...headline}>Transparent, free, and open.</Heading.h2>
        <Text my={3} {...subtext}>
          Hack Club is a new kind of non-profit with{' '}
          total transparency. We open source all of our{' '}
          content
          {' and '}
          code. Many of our leaders
          and members are also available on{' '}
          our Slack.
        </Text>
        <Text my={3} {...subtext}>
          You deserve to know exactly where your contribution will go—so{' '}

            all of our financials are public
          .
        </Text>
      </Container>
      <Row my={5} {...content}>
        <Box>
          <Heading.h2 {...headline}>
            Every last contribution is for our clubs.
          </Heading.h2>
          <ContributionShapes>
            <Pentagon size={128} rotate={16} />
          </ContributionShapes>
        </Box>
        <Box>
          <Text {...subtext}>
            We strive to build a maximally-efficient organization, spending 98%
            of funds given directly on our clubs.
          </Text>
          <Text mt={3} {...subtext}>
            When you give to Hack Club, your money goes where students need it
            most.
          </Text>
          <Financials mt={4}>
            <Box bg="primary" color="white" p={[3, 4]} pr={2}>
              <Heading.h3 f={4} mb={2} caps>
                Financials
              </Heading.h3>
              <Stats>
                <Stat
                  f={6}
                  value={commaNumber(stats.monthly)}
                  label="monthly spend"
                  mb={3}
                  w={1}
                />
                <Stat f={6} mb={2} value={stats.club} label="per club" />
                <Stat f={6} value={stats.student} label="per student" />
              </Stats>
            </Box>
            <Box bg="snow" p={[3, 4]}>
              <Heading.h3 f={4} my={0} caps>
                Spending breakdown
              </Heading.h3>
              <Spent />
            </Box>
          </Financials>
        </Box>
      </Row>
      <Container {...content}>
        <Heading.h2 {...headline}>
          Contribute more than just dollars.
        </Heading.h2>
        <Text my={3} {...subtext}>
          We accept donations of time, technical or hard science fiction books,
          stocks/other securities, and cryptocurrency.
        </Text>
        <Text my={3} {...subtext}>
          Please get in touch at{' '}
          donate@hackclub.com if you’re
          interested in making a contribution or have any questions.
        </Text>
      </Container>
      <Container {...content} align="center" mx="auto">
        <Heading.h2 {...headline} mt={5} mb={4}>
          A few of our amazing donors.
        </Heading.h2>
        <Flex m={-2} wrap>
          {Object.keys(donors).map(name => (
            <DonorListing key={name} name={name} url={donors[name]} />
          ))}
        </Flex>
        <Heading.h3 {...subhline} mt={4} mb={5}>
          and many more...
        </Heading.h3>
      </Container>
    </Container>

  </Fragment>
)
<commit_msg>Simplify donor list<commit_after>import React, { Fragment } from 'react'
import {
  Heading,
  Container,
  Card,
  Flex,
  Box,
  Text,
  LargeButton,
  Icon,
  Section,
  Link as A
} from '@hackclub/design-system'
import Helmet from 'react-helmet'
import Link from 'gatsby-link'
import Nav from 'components/Nav'
import Stat from 'components/Stat'
import {
  Triangle,
  Hexagon,
  Pentagon,
  Square,
  Circle,
  Line
} from 'components/Shapes'
import DonateForm from 'components/donate/DonateForm'
import Spent from 'components/donate/Spent'
import Footer from 'components/Footer'
import commaNumber from 'comma-number'
import donors from 'components/donate/donors.json'

const Header = Section.withComponent('header').extend`
  background: url('/pattern.svg');
  > div {
    display: grid;
    grid-gap: ${({ theme }) => theme.space[4]}px;
    ${({ theme }) => theme.mediaQueries.md} {
      grid-template-columns: 3fr 2fr;
    }
  }
`

const Row = Box.extend`
  text-align: left;
  ${({ theme }) => theme.mediaQueries.md} {
    display: grid;
    grid-gap: ${({ theme }) => theme.space[3]}px;
    grid-template-columns: ${({ reverse }) =>
      reverse ? '3fr 2fr' : '2fr 3fr'};
  }
`

const Financials = Box.extend`
  display: grid;
  border-radius: ${({ theme }) => theme.radius};
  overflow: hidden;

  ${({ theme }) => theme.mediaQueries.md} {
    grid-template-columns: 2fr 3fr;
  }
`

const Stats = Box.extend`
  div {
    width: 100%;
    display: block;
    text-align: left !important;
  }
  span:before {
    content: '$';
    font-size: ${({ theme }) => theme.fontSizes[4]}px;
    margin-left: -12px;
    vertical-align: super;
  }
  p {
    color: ${({ theme }) => theme.colors.red[1]};
  }
`

const Shapes = Box.extend`
  display: none;
  ${({ theme }) => theme.mediaQueries.md} {
    display: block;
    float: right;
    position: relative;
    z-index: -1;
    svg {
      position: absolute;
    }
  }
`

const WishShapes = Shapes.extend`
  svg {
    &:first-child {
      top: 0;
      right: 0;
      color: rgba(115, 45, 228, 0.75);
      z-index: 1;
    }
    &:last-child {
      top: 0;
      right: 4rem;
      color: rgba(45, 228, 207, 0.75);
    }
  }
`
const ContributionShapes = Shapes.extend`
  svg {
    right: 12rem;
    top: 1rem;
    color: ${({ theme }) => theme.colors.pink[4]};
  }
`
const DonorsShapes = Shapes.extend`
  svg:first-child {
    right: 8rem;
    top: 2rem;
    color: ${({ theme }) => theme.colors.orange[4]};
  }
  svg:last-child {
    right: 4rem;
    top: 4rem;
    color: ${({ theme }) => theme.hexa('blue.4', 0.75)};
  }
`

const headline = { f: [5, 6], mb: 3, style: { lineHeight: '1.125' } }
const subhline = { f: [3, 4], style: { lineHeight: '1.375' } }
const subtext = { f: [3, 4], style: { lineHeight: '1.5' } }

const contentContainer = {
  maxWidth: 64,
  w: 1,
  p: 3,
  color: 'black',
  style: { position: 'relative' }
}
const content = { maxWidth: 48, mx: 0, color: 'black' }

A.link = A.withComponent(Link)

const title = 'Donate to Hack Club'
const description =
  'Contribute today to empower the next generation and help start a coding club at every high school.'

const stats = {
  monthly: 6742.71,
  student: 3,
  club: 60
}

const DonorCard = ({ name, link = false }) => (
  <Card p={3} m={2} color="black">
    <Text
      align="center"
      justify="center"
      style={{ textDecoration: link ? 'underline' : 'none' }}
    >
      {name}
    </Text>
  </Card>
)

const DonorListing = ({ name, url }) => (
  <Box w={[1 / 2, 1 / 3]}>
    {url ? (

        <DonorCard name={name} link={true} />

    ) : (
      <DonorCard name={name} />
    )}
  </Box>
)

export default () => (
  <Fragment>
    <Helmet
      title={title}
      meta={[
        { name: 'twitter:title', content: title },
        { name: 'description', content: description },
        { name: 'twitter:description', content: description },
        { property: 'og:title', content: title },
        { property: 'og:description', content: description },
        { property: 'og:url', content: 'https://hackclub.com/donate' }
      ]}
    />

      <Container {...contentContainer} align="left" py={[4]}>
        <Container maxWidth={36} mx={0}>
          <Heading.h1 color="primary" f={[3, 4]} caps>
            Donate to Hack Club
          </Heading.h1>
          <Heading.h2 my={3} {...headline}>
            We rely on people like you to bring coding to the world.
          </Heading.h2>
          <Text {...subhline}>
            Contribute today to empower the next generation. Help start a Hack
            Club at every high school.
          </Text>
          <Text mt={3} f={2} color="muted">
            Your contribution is tax-deductible.

            Hack Club is a 501(c)(3) non-profit with the EIN 81-2908499.
          </Text>
        </Container>
        <Card
          bg="snow"
          p={[3, 4]}
          mt={[0, -3, -4, -5]}
          style={{ overflow: 'hidden' }}
        >
          <DonateForm />
        </Card>
      </Container>

    <Container {...contentContainer}>
      <WishShapes mt={3}>
        <Triangle size={128} rotate={64} />
        <Circle size={128} />
      </WishShapes>
      <Container {...content}>
        <Heading.h2 {...headline}>Transparent, free, and open.</Heading.h2>
        <Text my={3} {...subtext}>
          Hack Club is a new kind of non-profit with{' '}
          total transparency. We open source all of our{' '}
          content
          {' and '}
          code. Many of our leaders
          and members are also available on{' '}
          our Slack.
        </Text>
        <Text my={3} {...subtext}>
          You deserve to know exactly where your contribution will go—so{' '}

            all of our financials are public
          .
        </Text>
      </Container>
      <Row my={5} {...content}>
        <Box>
          <Heading.h2 {...headline}>
            Every last contribution is for our clubs.
          </Heading.h2>
          <ContributionShapes>
            <Pentagon size={128} rotate={16} />
          </ContributionShapes>
        </Box>
        <Box>
          <Text {...subtext}>
            We strive to build a maximally-efficient organization, spending 98%
            of funds given directly on our clubs.
          </Text>
          <Text mt={3} {...subtext}>
            When you give to Hack Club, your money goes where students need it
            most.
          </Text>
          <Financials mt={4}>
            <Box bg="primary" color="white" p={[3, 4]} pr={2}>
              <Heading.h3 f={4} mb={2} caps>
                Financials
              </Heading.h3>
              <Stats>
                <Stat
                  f={6}
                  value={commaNumber(stats.monthly)}
                  label="monthly spend"
                  mb={3}
                  w={1}
                />
                <Stat f={6} mb={2} value={stats.club} label="per club" />
                <Stat f={6} value={stats.student} label="per student" />
              </Stats>
            </Box>
            <Box bg="snow" p={[3, 4]}>
              <Heading.h3 f={4} my={0} caps>
                Spending breakdown
              </Heading.h3>
              <Spent />
            </Box>
          </Financials>
        </Box>
      </Row>
      <Container {...content}>
        <Heading.h2 {...headline}>
          Contribute more than just dollars.
        </Heading.h2>
        <Text my={3} {...subtext}>
          We accept donations of time, technical or hard science fiction books,
          stocks/other securities, and cryptocurrency.
        </Text>
        <Text my={3} {...subtext}>
          Please get in touch at{' '}
          donate@hackclub.com if you’re
          interested in making a contribution or have any questions.
        </Text>
      </Container>
      <Container {...content} align="center" mx="auto">
        <Heading.h2 {...headline} mt={5} mb={4}>
          A few of our amazing donors.
        </Heading.h2>
        <Flex m={-2} wrap>
          {Object.keys(donors).map(name => (
            <DonorListing key={name} name={name} url={donors[name]} />
          ))}
        </Flex>
        <Heading.h3 {...subhline} mt={4} mb={5}>
          and many more...
        </Heading.h3>
      </Container>
    </Container>

  </Fragment>
)
<|endoftext|>


<commit_before>import PropTypes from 'prop-types';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { reduxForm, Field } from 'redux-form';
import MenuItem from '@material-ui/core/MenuItem';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import { Row, Col } from 'react-flexbox-grid/lib';
import withIntlForm from '../../common/hocs/IntlForm';
import AppButton from '../../common/AppButton';
import messages from '../../../resources/messages';

const localMessages = {
  buttonLabel: { id: 'topic.version.generate.trigger', defaultMessage: 'Advanced Options...' },
  title: { id: 'topic.version.generate.dialog.title', defaultMessage: 'Generate Version: Admin Options' },
  intro: { id: 'topic.version.generate.dialog.intro', defaultMessage: 'As an admin you have a few helpful shortcuts you can use. You can decided to generate a new version (like normal),  or you can regenerate in the current version. This can be helpful if you just want to add a new subtopic or something. In additon, you can decided whether you want to respider or not. Thi can be helpful on big topics that perhaps need new subtopics or settings, but don\'t need to be respidered (because that would take a long time).' },
  newVersion: { id: 'topic.version.generate.newVersion.label', defaultMessage: 'Version:' },
  newVersionYes: { id: 'topic.version.generate.newVersion.yes', defaultMessage: 'Make a new version (like normal)' },
  newVersionNo: { id: 'topic.version.generate.newVersion.no', defaultMessage: 'Use the current (it will go back to queued status)' },
  spider: { id: 'topic.version.generate.respider.label', defaultMessage: 'Spidering' },
  spiderYes: { id: 'topic.version.generate.respider.yes', defaultMessage: 'Spider (like normal)' },
  spiderNo: { id: 'topic.version.generate.respider.no', defaultMessage: 'Don\'t spider (faster, no new stories)' },
};

export const FIELD_NEW_VERSION = 'newVersion';
export const FIELD_SPIDER = 'spider';

class GenerateVersionAdminDialog extends React.Component {
  state = {
    open: false,
  }

  handleGenerateRequest(values) {
    const { onGenerate } = this.props;
    this.setState({ open: false });
    onGenerate(values);
  }

  render() {
    const { intl, handleSubmit, renderSelect } = this.props;
    let content;
    if (!this.state.open) {
      content = <AppButton label={localMessages.buttonLabel} onClick={() => this.setState({ open: true })} />;
    } else {
      content = (
        <form
          className="app-form topic-generate-verison-admin-form"
          id="topicGenerateVersionAdminForm"
          name="topicGenerateVersionAdminForm"
          onSubmit={handleSubmit(this.handleGenerateRequest.bind(this))}
        >
          <Dialog
            className="app-dialog"
            open={this.state.open}
            onClose={() => this.setState({ open: false })}
          >
            <DialogTitle>{intl.formatMessage(localMessages.title)}</DialogTitle>
            <DialogContent>

<FormattedMessage {...localMessages.intro} />

              <Row>
                <Col lg={12}>

<FormattedMessage {...localMessages.newVersion} />

                  <Field name={FIELD_NEW_VERSION} component={renderSelect} type="inline" fullWidth>
                    <MenuItem value={1}><FormattedMessage {...localMessages.newVersionYes} /></MenuItem>
                    <MenuItem value={0}><FormattedMessage {...localMessages.newVersionNo} /></MenuItem>
                  </Field>
                </Col>
              </Row>
              <Row>
                <Col lg={12}>

<FormattedMessage {...localMessages.spider} />

                  <Field name={FIELD_SPIDER} component={renderSelect} type="inline" fullWidth>
                    <MenuItem value={1}><FormattedMessage {...localMessages.spiderYes} /></MenuItem>
                    <MenuItem value={0}><FormattedMessage {...localMessages.spiderNo} /></MenuItem>
                  </Field>
                </Col>
              </Row>
            </DialogContent>
            <DialogActions>
              <AppButton
                label={messages.cancel}
                onClick={() => this.setState({ open: false })}
              />
              <AppButton
                label={messages.runThisVersion}
                primary
                type="submit"
                form="topicGenerateVersionAdminForm"
              />
            </DialogActions>
          </Dialog>
        </form>
      );
    }
    return content;
  }
}

GenerateVersionAdminDialog.propTypes = {
  // from parent
  onGenerate: PropTypes.func.isRequired,
  initialValues: PropTypes.object,
  // from context
  intl: PropTypes.object.isRequired,
  renderSelect: PropTypes.func.isRequired,
  // from form healper
  handleSubmit: PropTypes.func,
  pristine: PropTypes.bool.isRequired,
  submitting: PropTypes.bool.isRequired,
};

const reduxFormConfig = {
  form: 'topicGenerateVersionAdminForm',
};

export default
withIntlForm(
  reduxForm(reduxFormConfig)(
    GenerateVersionAdminDialog
  ),
);
<commit_msg>Fixed typo<commit_after>import PropTypes from 'prop-types';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { reduxForm, Field } from 'redux-form';
import MenuItem from '@material-ui/core/MenuItem';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import { Row, Col } from 'react-flexbox-grid/lib';
import withIntlForm from '../../common/hocs/IntlForm';
import AppButton from '../../common/AppButton';
import messages from '../../../resources/messages';

const localMessages = {
  buttonLabel: { id: 'topic.version.generate.trigger', defaultMessage: 'Advanced Options...' },
  title: { id: 'topic.version.generate.dialog.title', defaultMessage: 'Generate Version: Admin Options' },
  intro: { id: 'topic.version.generate.dialog.intro', defaultMessage: 'As an admin you have a few helpful shortcuts you can use. You can decided to generate a new version (like normal),  or you can regenerate in the current version. This can be helpful if you just want to add a new subtopic or something. In additon, you can decided whether you want to respider or not. This can be helpful on big topics that perhaps need new subtopics or settings, but don\'t need to be respidered (because that would take a long time).' },
  newVersion: { id: 'topic.version.generate.newVersion.label', defaultMessage: 'Version:' },
  newVersionYes: { id: 'topic.version.generate.newVersion.yes', defaultMessage: 'Make a new version (like normal)' },
  newVersionNo: { id: 'topic.version.generate.newVersion.no', defaultMessage: 'Use the current (it will go back to queued status)' },
  spider: { id: 'topic.version.generate.respider.label', defaultMessage: 'Spidering' },
  spiderYes: { id: 'topic.version.generate.respider.yes', defaultMessage: 'Spider (like normal)' },
  spiderNo: { id: 'topic.version.generate.respider.no', defaultMessage: 'Don\'t spider (faster, no new stories)' },
};

export const FIELD_NEW_VERSION = 'newVersion';
export const FIELD_SPIDER = 'spider';

class GenerateVersionAdminDialog extends React.Component {
  state = {
    open: false,
  }

  handleGenerateRequest(values) {
    const { onGenerate } = this.props;
    this.setState({ open: false });
    onGenerate(values);
  }

  render() {
    const { intl, handleSubmit, renderSelect } = this.props;
    let content;
    if (!this.state.open) {
      content = <AppButton label={localMessages.buttonLabel} onClick={() => this.setState({ open: true })} />;
    } else {
      content = (
        <form
          className="app-form topic-generate-verison-admin-form"
          id="topicGenerateVersionAdminForm"
          name="topicGenerateVersionAdminForm"
          onSubmit={handleSubmit(this.handleGenerateRequest.bind(this))}
        >
          <Dialog
            className="app-dialog"
            open={this.state.open}
            onClose={() => this.setState({ open: false })}
          >
            <DialogTitle>{intl.formatMessage(localMessages.title)}</DialogTitle>
            <DialogContent>

<FormattedMessage {...localMessages.intro} />

              <Row>
                <Col lg={12}>

<FormattedMessage {...localMessages.newVersion} />

                  <Field name={FIELD_NEW_VERSION} component={renderSelect} type="inline" fullWidth>
                    <MenuItem value={1}><FormattedMessage {...localMessages.newVersionYes} /></MenuItem>
                    <MenuItem value={0}><FormattedMessage {...localMessages.newVersionNo} /></MenuItem>
                  </Field>
                </Col>
              </Row>
              <Row>
                <Col lg={12}>

<FormattedMessage {...localMessages.spider} />

                  <Field name={FIELD_SPIDER} component={renderSelect} type="inline" fullWidth>
                    <MenuItem value={1}><FormattedMessage {...localMessages.spiderYes} /></MenuItem>
                    <MenuItem value={0}><FormattedMessage {...localMessages.spiderNo} /></MenuItem>
                  </Field>
                </Col>
              </Row>
            </DialogContent>
            <DialogActions>
              <AppButton
                label={messages.cancel}
                onClick={() => this.setState({ open: false })}
              />
              <AppButton
                label={messages.runThisVersion}
                primary
                type="submit"
                form="topicGenerateVersionAdminForm"
              />
            </DialogActions>
          </Dialog>
        </form>
      );
    }
    return content;
  }
}

GenerateVersionAdminDialog.propTypes = {
  // from parent
  onGenerate: PropTypes.func.isRequired,
  initialValues: PropTypes.object,
  // from context
  intl: PropTypes.object.isRequired,
  renderSelect: PropTypes.func.isRequired,
  // from form healper
  handleSubmit: PropTypes.func,
  pristine: PropTypes.bool.isRequired,
  submitting: PropTypes.bool.isRequired,
};

const reduxFormConfig = {
  form: 'topicGenerateVersionAdminForm',
};

export default
withIntlForm(
  reduxForm(reduxFormConfig)(
    GenerateVersionAdminDialog
  ),
);
<|endoftext|>


<commit_before>// Set listeners for rendering based on user status.
// Listener that triggers when a new or different user signs in.
var userChanged = setIcons;

// Listener that triggers when sign-in status changes (but not when user changes).
var signInChanged = function(signedIn) {
  if (!signedIn) {
    const recipeCards = document.getElementsByClassName("recipe-card");
    for (let i = 0; i < recipeCards.length; i++) {
      let recipeCard = recipeCards[i];
      const addToCookbookButton = recipeCard.getElementsByClassName("add-to-cookbook-btn")[0];
      const addToPlannerButton = recipeCard.getElementsByClassName("add-to-planner-btn")[0];
      // removes the checkmark (nothing happens if the class does not exist)
      addToCookbookButton.classList.remove("fa-check");
      addToPlannerButton.classList.remove("fa-check");
      // adds the plus sign (nothing is added if it already exists)
      addToCookbookButton.classList.add("fa-plus");
      addToPlannerButton.classList.add("fa-plus");
    }
  }
}

/**
 * Takes the recipe info of a recipe as a JS object and the div that the recipe
 * card will be added to. Creates only one recipe card. For the method to work,
 * recipeInfo is expected to have the following data saved as properties,
 * regardless of the explicit property name:
 * a recipe name, a recipe description, and a recipe
 */
createRecipeCard = (divID, recipeInfo) => {
  let docDiv = document.getElementById(divID);

  let recipeDiv = createElement("div", "", {"class": "recipe-card"});
  let imageDiv = createElement("div", "", {"class": "image-div"});
  let textDiv = createElement("div", "", {"class": "text-div"});

  const id = recipeInfo["id"];
  const name = recipeInfo["name"];

  const addToPlannerButton = createElement("button", " Planner", {"class": "card-button action-button bottom more-left add-to-planner-btn fas fa-plus"});
  addToPlannerButton.addEventListener('click', () => manageList("add", id, name, "planner"));

  const addToCookbookButton = createElement("button", " Cookbook", {"class": "card-button action-button bottom more-right add-to-cookbook-btn fas fa-plus"});
  addToCookbookButton.addEventListener('click', () => manageList("add", id, name, "cookbook"));

  const removeFromPlannerButton = createElement("button", " Planner ", {"class": "card-button action-button bottom more-left fa fa-remove remove-from-planner-btn"});
  removeFromPlannerButton.addEventListener('click', () => manageList("remove", id, name, "planner"));
  removeFromPlannerButton.style.display = "none";

  const removeFromToCookbookButton = createElement("button", " Cookbook ", {"class": "card-button action-button bottom more-right fa fa-remove remove-from-cookbook-btn"});
  removeFromToCookbookButton.addEventListener('click', () => manageList("remove", id, name, "cookbook"));
  removeFromToCookbookButton.style.display = "none";

  let elementsToAddToImageDiv = [
    createImage(recipeInfo["name"], recipeInfo["imageBlobKey"]),
    addToPlannerButton, addToCookbookButton, removeFromPlannerButton, removeFromToCookbookButton,
  ];
  elementsToAddToImageDiv.forEach(elem => imageDiv.appendChild(elem));
  let elementsToAddToTextDiv = [
    createElement("button", recipeInfo["name"],
      {"class": "recipe-card-name",
      "onClick": "redirectRecipePage(" + recipeInfo["id"].toString() + ")"}),
    createElement("p", recipeInfo["id"], {"class": "hidden recipe-id"}),
    createElement("p", recipeInfo["description"], {"class": "recipe-card-description"}),
  ];
  elementsToAddToTextDiv.forEach(elem => textDiv.appendChild(elem));

  let allElementsToAdd = [imageDiv, textDiv];
  allElementsToAdd.forEach(elem => recipeDiv.appendChild(elem));
  docDiv.appendChild(recipeDiv);
}

/**
 * Takes the id for a recipe in Datastore, and redirects to the recipe page template
 * with the recipe ID as a search param.
 */
function redirectRecipePage(id) {
  window.location.assign("/pages/RecipePageTemplate.jsp?id=" + id.toString());

}

/**
 * Function for loading the recipe template page based on the id in the URL
 * search params.
 */
async function fillRecipeTemplate() {
  const recipeId = (new URL(document.location)).searchParams.get("id");

  // Gets the servlet data for the recipe
  // TODO: put this code in an if statement to handle recipe not found scenarios
  let response = await fetch("/search-id?recipeId=" + recipeId.toString());
  let responseText = await response.text();
  let recipeInfo = JSON.parse(responseText);

  let detailsSection = document.getElementById("details");
  let imageSection = document.getElementById("image-container");
  let descriptionSection = document.getElementById("description");
  let ingredientSection = document.getElementById("ingredients");
  let stepsSection = document.getElementById("steps");

  addRecipeInfo(recipeInfo, detailsSection);
  imageSection.appendChild(createImage(recipeInfo["name"], recipeInfo["imageBlobKey"]));
  descriptionSection.appendChild(createElement(
      "p", recipeInfo["description"], {"id": "description-text"}));
  addAsList(recipeInfo["ingredients"], ingredientSection, "Ingredients", false);
  addAsList(recipeInfo["steps"], stepsSection, "Steps", true);
}

/**
 * Creates the HTML for the information about the recipe and adds it to the div.
 * Current implementation adds the recipe name and the author.
 * Takes the recipe object and div (not div name) being used.
 */
addRecipeInfo = (recipeObj, infoDiv) => {
  let elementsForDiv = [
    createElement("h1", recipeObj["name"], {"id": "recipe-name"})/*,
    createElement("h3", recipeObj["author"], {"id": "recipe-author"})*/
    // Uncomment once the backend can handle sending author info too
  ];

  // Adds all the recipe info to the passed div
  elementsForDiv.forEach(elem => infoDiv.appendChild(elem));
}

/**
 * Creates the HTML for some part of the recipe which needs to shown as a list
 * (ingredients, steps, etc.) and adds it to the given div.
 * Takes a list and div (not div name) being used.
 */
addAsList = (listObj, listDiv, listName, ordered) => {
  let listType = "ol";
  if (ordered !== null && ordered === false) {
    listType = "ul";
  }

  // Constructs the base of how all item created in the function will be
  // named for CSS
  let cssRef = listName.toLowerCase() + "-list";

  // Creates the title for the list, and the list element
  let listForDiv = createElement(listType, "", {"id": cssRef})
  let listTitle = createElement("h2", listName, {"id": cssRef + "-title", "class": "list-header"});

  // Goes through all the objects in listObj and adds them as items to listForDiv
  listObj.forEach(item => listForDiv.appendChild(
    createElement("li", item, {"class": cssRef + "-item"})));

  listDiv.appendChild(listTitle);
  listDiv.appendChild(listForDiv);
}

/**
 * Takes an object which can be represented as a string or a string, an html
 * tag, and the html tag options stored as an object. Returns the reference to the
 * constructed html element.
 * Default values are set for object and tag options for instances where these aren't
 * needed, such as when making a new container div.
 */
createElement = (htmlTag, object = "", tagOptions = {}) => {
  // Create an HTML element with the given tag
  let htmlElement = document.createElement(htmlTag.toLowerCase());

  // If the object parameter is not null or an empty string, it
  // creates a textnode and adds it to the HTML element
  if (object !== null && object !== "") {
    let htmlText = document.createTextNode(object);
    htmlElement.appendChild(htmlText);
  }

  // If the tagOptions object is not null or empty, it goes through each
  // option and adds the attribute to the new HTML element.
  if (tagOptions !== null && Object.keys(tagOptions).length > 0) {
    for (opt in tagOptions) {
      htmlElement.setAttribute(opt, tagOptions[opt]);
    }
  }
  return htmlElement;
}

/**
 * Takes two objects which represent the name and the blobkey
 * Returns the reference to the constructed image element.
 */
createImage = (name, blobkey) => {
  let imageElement = document.createElement('img');
  imageElement.alt = name + " image";
  imageElement.className = "recipe-card-image";
  imageElement.src = "/serve?blobkey="+blobkey;
  return imageElement;
}

function setIcons() {
  const recipeCards = document.getElementsByClassName("recipe-card");
  for (let i = 0; i < recipeCards.length; i++) {
    const recipeCard = recipeCards[i];
    const recipeID = recipeCard.getElementsByClassName("recipe-id")[0].innerText;
    const addToPlannerButton = recipeCard.getElementsByClassName("add-to-planner-btn")[0];
    const addToCookbookButton = recipeCard.getElementsByClassName("add-to-cookbook-btn")[0];

    setIcon(addToPlannerButton, recipeID, "planner");
    setIcon(addToCookbookButton, recipeID, "cookbook");
  }
}

/**
 * Sets the icon of the button to a checkmark if already added to list
 * and a plus otherwise
 * Takes in button you wish to change, the id of the recipe its part of, and which
 * list the button is adding to
 */
function setIcon(button, id, type) {
  if (auth2 != null && auth2.isSignedIn.get()) {
    const idToken = getIdToken();
    // checks if the current recipe is in the user's planner
    fetch("/manage-list?id=" + id + "&idToken=" + idToken + "&type=" + type)
        .then(response => response.text()).then((contains) => {
      if ((/true/i).test(contains)) {
        button.classList.remove("fa-plus");
        button.classList.add("fa-check");
      }
    });
  }
}

// lets the user know if they have already added a recipe to a particular list
// otherwise adds the recipe
function manageList(action, id, name, type) {
  let idToken;
  confirmUser().then((googleUser) => {
    idToken = getIdToken();
    fetch("/manage-list?id=" + id + "&idToken=" + idToken + "&type=" + type)
        .then(response => response.text()).then((contains) => {
      if ((/true/i).test(contains)) {
        if (action == "add") {
          // if the user is trying to add a recipe and the list already contains it
          // alerts the user that the recipe is already in the list
          alert("You have already added the " + name + " recipe to your " + type);
        } else {
          // if the user is trying to remove a precipe and the list contains it
          // asks the user to confirm that they wish to remove the recipe
          const confirmedRemove = confirm("Are you sure you want to remove the " + name + " recipe from your " + type + "?");
          // tells the server to remove the recipe's key from the user's cookbook/planner
          if (confirmedRemove) {
            const params = new URLSearchParams();
            params.append("action", action);
            params.append("id", id);
            params.append("idToken", getIdToken());
            params.append("type", type);
            fetch("/manage-list", {method: "POST", body: params});
            if (document.URL.includes("UserPage")) {
              location.reload();
            }
          }
        }
      } else {
        if (action == "add") {
          // if the user is trying to add a recipe not already in the list
          const params = new URLSearchParams();
          params.append("action", action);
          params.append("id", id);
          params.append("idToken", getIdToken());
          params.append("type", type);
          fetch("/manage-list", {method: "POST", body: params});
          // finds the button that was clicked using the recipe id
          let recipeCards = document.getElementsByClassName("recipe-card");
          for (let i = 0; i < recipeCards.length; i++) {
            let recipeID = recipeCards[i].getElementsByClassName("recipe-id")[0].innerText;
            let button = recipeCards[i].getElementsByClassName("add-to-" + type + "-btn")[0];
            if (recipeID == id) {
              // makes the icon a checkmark
              button.classList.remove("fa-plus");
              button.classList.add("fa-check");
              break;
            }
          }
          if (document.URL.includes("UserPage")) {
            location.reload();
          }
        }
      }
    });
  });
}
<commit_msg>Removed extra line<commit_after>// Set listeners for rendering based on user status.
// Listener that triggers when a new or different user signs in.
var userChanged = setIcons;

// Listener that triggers when sign-in status changes (but not when user changes).
var signInChanged = function(signedIn) {
  if (!signedIn) {
    const recipeCards = document.getElementsByClassName("recipe-card");
    for (let i = 0; i < recipeCards.length; i++) {
      let recipeCard = recipeCards[i];
      const addToCookbookButton = recipeCard.getElementsByClassName("add-to-cookbook-btn")[0];
      const addToPlannerButton = recipeCard.getElementsByClassName("add-to-planner-btn")[0];
      // removes the checkmark (nothing happens if the class does not exist)
      addToCookbookButton.classList.remove("fa-check");
      addToPlannerButton.classList.remove("fa-check");
      // adds the plus sign (nothing is added if it already exists)
      addToCookbookButton.classList.add("fa-plus");
      addToPlannerButton.classList.add("fa-plus");
    }
  }
}

/**
 * Takes the recipe info of a recipe as a JS object and the div that the recipe
 * card will be added to. Creates only one recipe card. For the method to work,
 * recipeInfo is expected to have the following data saved as properties,
 * regardless of the explicit property name:
 * a recipe name, a recipe description, and a recipe
 */
createRecipeCard = (divID, recipeInfo) => {
  let docDiv = document.getElementById(divID);

  let recipeDiv = createElement("div", "", {"class": "recipe-card"});
  let imageDiv = createElement("div", "", {"class": "image-div"});
  let textDiv = createElement("div", "", {"class": "text-div"});

  const id = recipeInfo["id"];
  const name = recipeInfo["name"];

  const addToPlannerButton = createElement("button", " Planner", {"class": "card-button action-button bottom more-left add-to-planner-btn fas fa-plus"});
  addToPlannerButton.addEventListener('click', () => manageList("add", id, name, "planner"));

  const addToCookbookButton = createElement("button", " Cookbook", {"class": "card-button action-button bottom more-right add-to-cookbook-btn fas fa-plus"});
  addToCookbookButton.addEventListener('click', () => manageList("add", id, name, "cookbook"));

  const removeFromPlannerButton = createElement("button", " Planner ", {"class": "card-button action-button bottom more-left fa fa-remove remove-from-planner-btn"});
  removeFromPlannerButton.addEventListener('click', () => manageList("remove", id, name, "planner"));
  removeFromPlannerButton.style.display = "none";

  const removeFromToCookbookButton = createElement("button", " Cookbook ", {"class": "card-button action-button bottom more-right fa fa-remove remove-from-cookbook-btn"});
  removeFromToCookbookButton.addEventListener('click', () => manageList("remove", id, name, "cookbook"));
  removeFromToCookbookButton.style.display = "none";

  let elementsToAddToImageDiv = [
    createImage(recipeInfo["name"], recipeInfo["imageBlobKey"]),
    addToPlannerButton, addToCookbookButton, removeFromPlannerButton, removeFromToCookbookButton,
  ];
  elementsToAddToImageDiv.forEach(elem => imageDiv.appendChild(elem));
  let elementsToAddToTextDiv = [
    createElement("button", recipeInfo["name"],
      {"class": "recipe-card-name",
      "onClick": "redirectRecipePage(" + recipeInfo["id"].toString() + ")"}),
    createElement("p", recipeInfo["id"], {"class": "hidden recipe-id"}),
    createElement("p", recipeInfo["description"], {"class": "recipe-card-description"}),
  ];
  elementsToAddToTextDiv.forEach(elem => textDiv.appendChild(elem));

  let allElementsToAdd = [imageDiv, textDiv];
  allElementsToAdd.forEach(elem => recipeDiv.appendChild(elem));
  docDiv.appendChild(recipeDiv);
}

/**
 * Takes the id for a recipe in Datastore, and redirects to the recipe page template
 * with the recipe ID as a search param.
 */
function redirectRecipePage(id) {
  window.location.assign("/pages/RecipePageTemplate.jsp?id=" + id.toString());
}

/**
 * Function for loading the recipe template page based on the id in the URL
 * search params.
 */
async function fillRecipeTemplate() {
  const recipeId = (new URL(document.location)).searchParams.get("id");

  // Gets the servlet data for the recipe
  // TODO: put this code in an if statement to handle recipe not found scenarios
  let response = await fetch("/search-id?recipeId=" + recipeId.toString());
  let responseText = await response.text();
  let recipeInfo = JSON.parse(responseText);

  let detailsSection = document.getElementById("details");
  let imageSection = document.getElementById("image-container");
  let descriptionSection = document.getElementById("description");
  let ingredientSection = document.getElementById("ingredients");
  let stepsSection = document.getElementById("steps");

  addRecipeInfo(recipeInfo, detailsSection);
  imageSection.appendChild(createImage(recipeInfo["name"], recipeInfo["imageBlobKey"]));
  descriptionSection.appendChild(createElement(
      "p", recipeInfo["description"], {"id": "description-text"}));
  addAsList(recipeInfo["ingredients"], ingredientSection, "Ingredients", false);
  addAsList(recipeInfo["steps"], stepsSection, "Steps", true);
}

/**
 * Creates the HTML for the information about the recipe and adds it to the div.
 * Current implementation adds the recipe name and the author.
 * Takes the recipe object and div (not div name) being used.
 */
addRecipeInfo = (recipeObj, infoDiv) => {
  let elementsForDiv = [
    createElement("h1", recipeObj["name"], {"id": "recipe-name"})/*,
    createElement("h3", recipeObj["author"], {"id": "recipe-author"})*/
    // Uncomment once the backend can handle sending author info too
  ];

  // Adds all the recipe info to the passed div
  elementsForDiv.forEach(elem => infoDiv.appendChild(elem));
}

/**
 * Creates the HTML for some part of the recipe which needs to shown as a list
 * (ingredients, steps, etc.) and adds it to the given div.
 * Takes a list and div (not div name) being used.
 */
addAsList = (listObj, listDiv, listName, ordered) => {
  let listType = "ol";
  if (ordered !== null && ordered === false) {
    listType = "ul";
  }

  // Constructs the base of how all item created in the function will be
  // named for CSS
  let cssRef = listName.toLowerCase() + "-list";

  // Creates the title for the list, and the list element
  let listForDiv = createElement(listType, "", {"id": cssRef})
  let listTitle = createElement("h2", listName, {"id": cssRef + "-title", "class": "list-header"});

  // Goes through all the objects in listObj and adds them as items to listForDiv
  listObj.forEach(item => listForDiv.appendChild(
    createElement("li", item, {"class": cssRef + "-item"})));

  listDiv.appendChild(listTitle);
  listDiv.appendChild(listForDiv);
}

/**
 * Takes an object which can be represented as a string or a string, an html
 * tag, and the html tag options stored as an object. Returns the reference to the
 * constructed html element.
 * Default values are set for object and tag options for instances where these aren't
 * needed, such as when making a new container div.
 */
createElement = (htmlTag, object = "", tagOptions = {}) => {
  // Create an HTML element with the given tag
  let htmlElement = document.createElement(htmlTag.toLowerCase());

  // If the object parameter is not null or an empty string, it
  // creates a textnode and adds it to the HTML element
  if (object !== null && object !== "") {
    let htmlText = document.createTextNode(object);
    htmlElement.appendChild(htmlText);
  }

  // If the tagOptions object is not null or empty, it goes through each
  // option and adds the attribute to the new HTML element.
  if (tagOptions !== null && Object.keys(tagOptions).length > 0) {
    for (opt in tagOptions) {
      htmlElement.setAttribute(opt, tagOptions[opt]);
    }
  }
  return htmlElement;
}

/**
 * Takes two objects which represent the name and the blobkey
 * Returns the reference to the constructed image element.
 */
createImage = (name, blobkey) => {
  let imageElement = document.createElement('img');
  imageElement.alt = name + " image";
  imageElement.className = "recipe-card-image";
  imageElement.src = "/serve?blobkey="+blobkey;
  return imageElement;
}

function setIcons() {
  const recipeCards = document.getElementsByClassName("recipe-card");
  for (let i = 0; i < recipeCards.length; i++) {
    const recipeCard = recipeCards[i];
    const recipeID = recipeCard.getElementsByClassName("recipe-id")[0].innerText;
    const addToPlannerButton = recipeCard.getElementsByClassName("add-to-planner-btn")[0];
    const addToCookbookButton = recipeCard.getElementsByClassName("add-to-cookbook-btn")[0];

    setIcon(addToPlannerButton, recipeID, "planner");
    setIcon(addToCookbookButton, recipeID, "cookbook");
  }
}

/**
 * Sets the icon of the button to a checkmark if already added to list
 * and a plus otherwise
 * Takes in button you wish to change, the id of the recipe its part of, and which
 * list the button is adding to
 */
function setIcon(button, id, type) {
  if (auth2 != null && auth2.isSignedIn.get()) {
    const idToken = getIdToken();
    // checks if the current recipe is in the user's planner
    fetch("/manage-list?id=" + id + "&idToken=" + idToken + "&type=" + type)
        .then(response => response.text()).then((contains) => {
      if ((/true/i).test(contains)) {
        button.classList.remove("fa-plus");
        button.classList.add("fa-check");
      }
    });
  }
}

// lets the user know if they have already added a recipe to a particular list
// otherwise adds the recipe
function manageList(action, id, name, type) {
  let idToken;
  confirmUser().then((googleUser) => {
    idToken = getIdToken();
    fetch("/manage-list?id=" + id + "&idToken=" + idToken + "&type=" + type)
        .then(response => response.text()).then((contains) => {
      if ((/true/i).test(contains)) {
        if (action == "add") {
          // if the user is trying to add a recipe and the list already contains it
          // alerts the user that the recipe is already in the list
          alert("You have already added the " + name + " recipe to your " + type);
        } else {
          // if the user is trying to remove a precipe and the list contains it
          // asks the user to confirm that they wish to remove the recipe
          const confirmedRemove = confirm("Are you sure you want to remove the " + name + " recipe from your " + type + "?");
          // tells the server to remove the recipe's key from the user's cookbook/planner
          if (confirmedRemove) {
            const params = new URLSearchParams();
            params.append("action", action);
            params.append("id", id);
            params.append("idToken", getIdToken());
            params.append("type", type);
            fetch("/manage-list", {method: "POST", body: params});
            if (document.URL.includes("UserPage")) {
              location.reload();
            }
          }
        }
      } else {
        if (action == "add") {
          // if the user is trying to add a recipe not already in the list
          const params = new URLSearchParams();
          params.append("action", action);
          params.append("id", id);
          params.append("idToken", getIdToken());
          params.append("type", type);
          fetch("/manage-list", {method: "POST", body: params});
          // finds the button that was clicked using the recipe id
          let recipeCards = document.getElementsByClassName("recipe-card");
          for (let i = 0; i < recipeCards.length; i++) {
            let recipeID = recipeCards[i].getElementsByClassName("recipe-id")[0].innerText;
            let button = recipeCards[i].getElementsByClassName("add-to-" + type + "-btn")[0];
            if (recipeID == id) {
              // makes the icon a checkmark
              button.classList.remove("fa-plus");
              button.classList.add("fa-check");
              break;
            }
          }
          if (document.URL.includes("UserPage")) {
            location.reload();
          }
        }
      }
    });
  });
}
<|endoftext|>


<commit_before>module.exports = {
    "env": {
        "browser": true,
        "es6": true,
        "jest/globals": true,
    },
    "extends": "airbnb-base",
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly",
    },
    "parserOptions": {
        "ecmaVersion": 2018
    },
    "plugins": ["jest"],
    "rules": {
        "no-unused-vars": 1,
        "no-param-reassign": ["error", { "props": false }],
        "no-underscore-dangle": 0,
        "jest/no-disabled-tests": "warn",
        "jest/no-focused-tests": "error",
        "jest/no-identical-title": "error",
        "jest/prefer-to-have-length": "warn",
        "jest/valid-expect": "error",
    }
};
<commit_msg>lint to lint settings. Allow for...of<commit_after>module.exports = {
  env: {
    browser: true,
    es6: true,
    'jest/globals': true,
  },
  extends: 'airbnb-base',
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly',
  },
  parserOptions: {
    ecmaVersion: 2018,
  },
  plugins: ['jest'],
  rules: {
    'no-unused-vars': 1,
    'no-param-reassign': ['error', { props: false }],
    'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'],
    'jest/no-disabled-tests': 'warn',
    'jest/no-focused-tests': 'error',
    'jest/no-identical-title': 'error',
    'jest/prefer-to-have-length': 'warn',
    'jest/valid-expect': 'error',
  },
};
<|endoftext|>


<commit_before>const schedule = require('node-schedule');
const {promises: fs, constants} = require('fs');
const path = require('path');
const moment = require('moment');
const { clamp, random } = require('lodash');

// どうしてこれらのojiginekoが選ばれたかって？僕がこいつらがかわいいとおもったからさ。
const ojiginekoActivities = [
	'dot-ojigineko',
	'harassment-ojigineko',
	'nameraka-ojigineko',
	'nameraka-ojigineko-extreme-fast',
	'ojigineko',
	'ojigineko-extremefast',
	'ojigineko-fast',
	'ojigineko-hd',
	'ojigineko-mirror',
	'ojigineko-muscle-exercise',
	'ojigineko-sleeping',
	'ojigineko-superfast',
	'ojigineko-upside-down',
	'ojigineko-waking',
	'party-ojigineko',
	'tosshutsu-symmetry-ojigineko',
];

const statePath = path.resolve(__dirname, 'state.json');

const loadState = async () => {
	const exists = await fs.access(statePath, constants.F_OK).then(() => true).catch(() => false);

	if (exists) {
		const stateData = await fs.readFile(statePath);
		return JSON.parse(stateData.toString());
	}

	return {
		isSleeping: false,
		location: 8,
	};
};

module.exports = async (clients) => {
	const { rtmClient: rtm, webClient: slack } = clients;
	const state = await loadState();

	const setState = async (newState) => {
		Object.assign(state, newState);
		await fs.writeFile(statePath, JSON.stringify(state));
	};

	schedule.scheduleJob('30 * * * *', async (date) => {
		const hour = moment(date).utcOffset(9).hour();

		if (state.isSleeping) {
			if (
				(hour === 6 && Math.random() < 0.05) ||
				(hour === 7 && Math.random() < 0.1) ||
				(hour === 8 && Math.random() < 0.2) ||
				(hour === 9 && Math.random() < 0.4) ||
				(hour === 10 && Math.random() < 0.7) ||
				(hour === 11 && Math.random() < 0.9) ||
				(hour === 12)
			) {
				await setState({ isSleeping: false });
			}
		}

		if (!state.isSleeping) {
			if (
				(hour === 21 && Math.random() < 0.05) ||
				(hour === 22 && Math.random() < 0.1) ||
				(hour === 23 && Math.random() < 0.2) ||
				(hour === 0 && Math.random() < 0.4) ||
				(hour === 1 && Math.random() < 0.7) ||
				(hour === 2 && Math.random() < 0.9) ||
				(hour === 3)
			) {
				await setState({ isSleeping: true });
			}
		}

		const ojiginekoActivity = state.isSleeping ? 'ojigineko-sleeping' : ojiginekoActivities[Math.floor(Math.random() * ojiginekoActivities.length)];

		if (!state.isSleeping) {
			setState({ location: clamp(state.location + random(-2, 2), 0, 16) });
		}

		await slack.chat.postMessage({
			channel: process.env.CHANNEL_OJIGINEKO,
			username: 'ojigineko',
			icon_emoji: ':pizzacat83:',
			text: `${':void:'.repeat(state.location)}:${ojiginekoActivity}:`,
		});
	});
}
<commit_msg>ojigineko-life: shogyomujo<commit_after>const schedule = require('node-schedule');
const {promises: fs, constants} = require('fs');
const path = require('path');
const moment = require('moment');
const { clamp, random } = require('lodash');

// どうしてこれらのojiginekoが選ばれたかって？僕がこいつらがかわいいとおもったからさ。
const ojiginekoActivities = [
	'dot-ojigineko',
	'harassment-ojigineko',
	'nameraka-ojigineko',
	'nameraka-ojigineko-extreme-fast',
	'ojigineko',
	'ojigineko-extremefast',
	'ojigineko-fast',
	'ojigineko-hd',
	'ojigineko-mirror',
	'ojigineko-muscle-exercise',
	'ojigineko-sleeping',
	'ojigineko-superfast',
	'ojigineko-upside-down',
	'ojigineko-waking',
	'party-ojigineko',
	'tosshutsu-symmetry-ojigineko',
];

const statePath = path.resolve(__dirname, 'state.json');

const loadState = async () => {
	const exists = await fs.access(statePath, constants.F_OK).then(() => true).catch(() => false);

	if (exists) {
		const stateData = await fs.readFile(statePath);
		return JSON.parse(stateData.toString());
	}

	return {
		isSleeping: false,
		location: 8,
		gone: false,
	};
};

module.exports = async (clients) => {
	const { rtmClient: rtm, webClient: slack } = clients;
	const state = await loadState();

	const setState = async (newState) => {
		Object.assign(state, newState);
		await fs.writeFile(statePath, JSON.stringify(state));
	};

	const p = 1 / (365 * 24);

	schedule.scheduleJob('30 * * * *', async (date) => {
		if (Math.random() < p)
			state.gone = true;
		if (state.gone)
			return;

		const hour = moment(date).utcOffset(9).hour();

		if (state.isSleeping) {
			if (
				(hour === 6 && Math.random() < 0.05) ||
				(hour === 7 && Math.random() < 0.1) ||
				(hour === 8 && Math.random() < 0.2) ||
				(hour === 9 && Math.random() < 0.4) ||
				(hour === 10 && Math.random() < 0.7) ||
				(hour === 11 && Math.random() < 0.9) ||
				(hour === 12)
			) {
				await setState({ isSleeping: false });
			}
		}

		if (!state.isSleeping) {
			if (
				(hour === 21 && Math.random() < 0.05) ||
				(hour === 22 && Math.random() < 0.1) ||
				(hour === 23 && Math.random() < 0.2) ||
				(hour === 0 && Math.random() < 0.4) ||
				(hour === 1 && Math.random() < 0.7) ||
				(hour === 2 && Math.random() < 0.9) ||
				(hour === 3)
			) {
				await setState({ isSleeping: true });
			}
		}

		const ojiginekoActivity = state.isSleeping ? 'ojigineko-sleeping' : ojiginekoActivities[Math.floor(Math.random() * ojiginekoActivities.length)];

		if (!state.isSleeping) {
			setState({ location: clamp(state.location + random(-2, 2), 0, 16) });
		}

		await slack.chat.postMessage({
			channel: process.env.CHANNEL_OJIGINEKO,
			username: 'ojigineko',
			icon_emoji: ':pizzacat83:',
			text: `${':void:'.repeat(state.location)}:${ojiginekoActivity}:`,
		});
	});
}
<|endoftext|>


<commit_before>/**
 * node-archiver
 *
 * Copyright (c) 2012-2014 Chris Talkington, contributors.
 * Licensed under the MIT license.
 * https://github.com/ctalkington/node-archiver/blob/master/LICENSE-MIT
 */
var fs = require('fs');
var inherits = require('util').inherits;
var Transform = require('readable-stream').Transform;

var async = require('async');

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

var Archiver = module.exports = function(options) {
  options = this.options = util.defaults(options, {
    highWaterMark: 1024 * 1024,
    statConcurrency: 4
  });

  Transform.call(this, options);

  this._entries = [];
  this._module = false;
  this._pointer = 0;

  this._queue = async.queue(this._onQueueTask.bind(this), 1);
  this._queue.drain = this._onQueueDrain.bind(this);

  this._statQueue = async.queue(this._onStatQueueTask.bind(this), options.statConcurrency);

  this._state = {
    aborted: false,
    finalize: false,
    finalizing: false,
    finalized: false,
    modulePiped: false
  };
};

inherits(Archiver, Transform);

Archiver.prototype._abort = function() {
  this._queue.kill();
  this._statQueue.kill();
  this._state.aborted = true;
};

Archiver.prototype._append = function(filepath, data) {
  data = data || {};

  var task = {
    source: null,
    filepath: filepath
  };

  if (!data.name) {
    data.name = filepath;
  }

  data.sourcePath = filepath;
  task.data = data;

  if (data.stats && data.stats instanceof fs.Stats) {
    task = this._updateQueueTaskWithStats(task, data.stats);
    this._queue.push(task);
  } else {
    this._statQueue.push(task);
  }
};

Archiver.prototype._moduleAppend = function(source, data, callback) {
  this._module.append(source, data, callback);
};

Archiver.prototype._moduleFinalize = function() {
  this._state.finalizing = true;

  if (typeof this._module.finalize === 'function') {
    this._module.finalize();
  } else if (typeof this._module.end === 'function') {
    this._module.end();
  } else {
    this.emit('error', new Error('format module missing finalize and end method'));
  }

  this._state.finalizing = false;
  this._state.finalized = true;
};

Archiver.prototype._moduleSupports = function(key) {
  this._module.supports = util.defaults(this._module.supports, {
    directory: false
  });

  return this._module.supports[key];
};

Archiver.prototype._normalizeEntryData = function(data, stats) {
  data = util.defaults(data, {
    type: 'file',
    name: null,
    date: null,
    mode: null,
    sourcePath: null,
    stats: false
  });

  if (stats && data.stats === false) {
    data.stats = stats;
  }

  var isDir = data.type === 'directory';

  if (data.name) {
    data.name = util.sanitizePath(data.name);

    if (data.name.slice(-1) === '/') {
      isDir = true;
      data.type = 'directory';
    } else if (isDir) {
      data.name += '/';
    }
  }

  if (typeof data.mode === 'number') {
    data.mode &= 0777;
  } else if (data.stats && data.mode === null) {
    data.mode = data.stats.mode & 0777;
  } else if (data.mode === null) {
    data.mode = isDir ? 0755 : 0644;
  }

  if (data.stats && data.date === null) {
    data.date = data.stats.mtime;
  } else {
    data.date = util.dateify(data.date);
  }

  return data;
};

Archiver.prototype._onModuleError = function(err) {
  this.emit('error', err);
};

Archiver.prototype._onQueueDrain = function() {
  if (this._state.finalizing || this._state.finalized || this._state.aborted) {
    return;
  }

  if (this._state.finalize && this._queue.idle() && this._statQueue.idle()) {
    this._moduleFinalize();
  }
};

Archiver.prototype._onQueueTask = function(task, callback) {
  this._task = task;

  this._moduleAppend(task.source, task.data, function(err, entry) {
    this._task = null;

    if (err) {
      this.emit('error', err);
      callback();
      return;
    }

    entry = entry || task.data;

    this.emit('entry', entry);
    this._entries.push(entry);

    callback();
  }.bind(this));
};

Archiver.prototype._onStatQueueTask = function(task, callback) {
  fs.stat(task.filepath, function(err, stats) {
    if (err) {
      this.emit('error', err);
      callback();
      return;
    }

    task = this._updateQueueTaskWithStats(task, stats);

    if (task.source !== null) {
      this._queue.push(task);
      callback();
    } else {
      this.emit('error', new Error('unsupported entry: ' + task.filepath));
      callback();
      return;
    }
  }.bind(this));
};

Archiver.prototype._updateQueueTaskWithStats = function(task, stats) {
  if (stats.isFile()) {
    task.data.type = 'file';
    task.data.sourceType = 'stream';
    task.source = util.lazyReadStream(task.filepath);
  } else if (stats.isDirectory() && this._moduleSupports('directory')) {
    task.data.name = util.trailingSlashIt(task.data.name);
    task.data.type = 'directory';
    task.data.sourcePath = util.trailingSlashIt(task.filepath);
    task.data.sourceType = 'buffer';
    task.source = new Buffer(0);
  } else {
    return task;
  }

  task.data = this._normalizeEntryData(task.data, stats);
  return task;
};

Archiver.prototype._pipeModuleOutput = function() {
  this._module.on('error', this._onModuleError.bind(this));
  this._module.pipe(this);

  this._state.modulePiped = true;
};

Archiver.prototype._processFile = function(source, data, callback) {
  this.emit('error', new Error('method not implemented'));
};

Archiver.prototype._transform = function(chunk, encoding, callback) {
  if (chunk) {
    this._pointer += chunk.length;
  }

  callback(null, chunk);
};

Archiver.prototype.abort = function() {
  if (this._state.aborted) {
    this.emit('error', new Error('abort: already aborted'));
    return this;
  }

  if (this._state.finalized) {
    this.emit('error', new Error('abort: already finalized'));
    return this;
  }

  this._abort();

  return this;
};

Archiver.prototype.append = function(source, data) {
  if (this._state.finalize || this._state.aborted) {
    this.emit('error', new Error('append: queue closed'));
    return this;
  }

  data = this._normalizeEntryData(data);

  if (typeof data.name !== 'string' || data.name.length === 0) {
    this.emit('error', new Error('append: entry name must be a non-empty string value'));
    return this;
  }

  if (data.type === 'directory' && !this._moduleSupports('directory')) {
    this.emit('error', new Error('append: entries of "directory" type not currently supported by this module'));
    return this;
  }

  source = util.normalizeInputSource(source);

  if (Buffer.isBuffer(source)) {
    data.sourceType = 'buffer';
  } else if (util.isStream(source)) {
    data.sourceType = 'stream';
  } else {
    this.emit('error', new Error('append: input source must be valid Stream or Buffer instance'));
    return this;
  }

  this._queue.push({
    data: data,
    source: source
  });

  return this;
};

Archiver.prototype.bulk = function(mappings) {
  if (this._state.finalize || this._state.aborted) {
    this.emit('error', new Error('bulk: queue closed'));
    return this;
  }

  if (!Array.isArray(mappings)) {
    mappings = [mappings];
  }

  var self = this;
  var files = util.file.normalizeFilesArray(mappings);

  files.forEach(function(file){
    var isExpandedPair = file.orig.expand || false;
    var fileData = file.data || {};

    file.src.forEach(function(filepath) {
      var data = util._.extend({}, fileData);
      data.name = isExpandedPair ? util.unixifyPath(file.dest) : util.unixifyPath(file.dest || '', filepath);

      if (data.name === '.') {
        return;
      }

      self._append(filepath, data);
    });
  });

  return this;
};

Archiver.prototype.file = function(filepath, data) {
  if (this._state.finalize || this._state.aborted) {
    this.emit('error', new Error('file: queue closed'));
    return this;
  }

  if (typeof filepath !== 'string' || filepath.length === 0) {
    this.emit('error', new Error('file: filepath must be a non-empty string value'));
    return this;
  }

  this._append(filepath, data);

  return this;
};

Archiver.prototype.finalize = function() {
  if (this._state.aborted) {
    this.emit('error', new Error('finalize: archive was aborted'));
    return this;
  }

  if (this._state.finalize) {
    this.emit('error', new Error('finalize: archive already finalizing'));
    return this;
  }

  this._state.finalize = true;

  if (this._queue.idle() && this._statQueue.idle()) {
    this._moduleFinalize();
  }

  return this;
};

Archiver.prototype.setModule = function(module) {
  if (this._state.aborted) {
    this.emit('error', new Error('module: archive was aborted'));
    return this;
  }

  if (this._state.modulePiped) {
    this.emit('error', new Error('module: module already set'));
    return;
  }

  this._module = module;
  this._pipeModuleOutput();
};

Archiver.prototype.pointer = function() {
  return this._pointer;
};<commit_msg>core: ensure that queue tasks exit early if called after abort or finalized.<commit_after>/**
 * node-archiver
 *
 * Copyright (c) 2012-2014 Chris Talkington, contributors.
 * Licensed under the MIT license.
 * https://github.com/ctalkington/node-archiver/blob/master/LICENSE-MIT
 */
var fs = require('fs');
var inherits = require('util').inherits;
var Transform = require('readable-stream').Transform;

var async = require('async');

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

var Archiver = module.exports = function(options) {
  options = this.options = util.defaults(options, {
    highWaterMark: 1024 * 1024,
    statConcurrency: 4
  });

  Transform.call(this, options);

  this._entries = [];
  this._module = false;
  this._pointer = 0;

  this._queue = async.queue(this._onQueueTask.bind(this), 1);
  this._queue.drain = this._onQueueDrain.bind(this);

  this._statQueue = async.queue(this._onStatQueueTask.bind(this), options.statConcurrency);

  this._state = {
    aborted: false,
    finalize: false,
    finalizing: false,
    finalized: false,
    modulePiped: false
  };
};

inherits(Archiver, Transform);

Archiver.prototype._abort = function() {
  this._queue.kill();
  this._statQueue.kill();
  this._state.aborted = true;
};

Archiver.prototype._append = function(filepath, data) {
  data = data || {};

  var task = {
    source: null,
    filepath: filepath
  };

  if (!data.name) {
    data.name = filepath;
  }

  data.sourcePath = filepath;
  task.data = data;

  if (data.stats && data.stats instanceof fs.Stats) {
    task = this._updateQueueTaskWithStats(task, data.stats);
    this._queue.push(task);
  } else {
    this._statQueue.push(task);
  }
};

Archiver.prototype._moduleAppend = function(source, data, callback) {
  this._module.append(source, data, callback);
};

Archiver.prototype._moduleFinalize = function() {
  this._state.finalizing = true;

  if (typeof this._module.finalize === 'function') {
    this._module.finalize();
  } else if (typeof this._module.end === 'function') {
    this._module.end();
  } else {
    this.emit('error', new Error('format module missing finalize and end method'));
  }

  this._state.finalizing = false;
  this._state.finalized = true;
};

Archiver.prototype._moduleSupports = function(key) {
  this._module.supports = util.defaults(this._module.supports, {
    directory: false
  });

  return this._module.supports[key];
};

Archiver.prototype._normalizeEntryData = function(data, stats) {
  data = util.defaults(data, {
    type: 'file',
    name: null,
    date: null,
    mode: null,
    sourcePath: null,
    stats: false
  });

  if (stats && data.stats === false) {
    data.stats = stats;
  }

  var isDir = data.type === 'directory';

  if (data.name) {
    data.name = util.sanitizePath(data.name);

    if (data.name.slice(-1) === '/') {
      isDir = true;
      data.type = 'directory';
    } else if (isDir) {
      data.name += '/';
    }
  }

  if (typeof data.mode === 'number') {
    data.mode &= 0777;
  } else if (data.stats && data.mode === null) {
    data.mode = data.stats.mode & 0777;
  } else if (data.mode === null) {
    data.mode = isDir ? 0755 : 0644;
  }

  if (data.stats && data.date === null) {
    data.date = data.stats.mtime;
  } else {
    data.date = util.dateify(data.date);
  }

  return data;
};

Archiver.prototype._onModuleError = function(err) {
  this.emit('error', err);
};

Archiver.prototype._onQueueDrain = function() {
  if (this._state.finalizing || this._state.finalized || this._state.aborted) {
    return;
  }

  if (this._state.finalize && this._queue.idle() && this._statQueue.idle()) {
    this._moduleFinalize();
  }
};

Archiver.prototype._onQueueTask = function(task, callback) {
  if (this._state.finalizing || this._state.finalized || this._state.aborted) {
    callback();
    return;
  }

  this._task = task;

  this._moduleAppend(task.source, task.data, function(err, entry) {
    this._task = null;

    if (err) {
      this.emit('error', err);
      callback();
      return;
    }

    entry = entry || task.data;

    this.emit('entry', entry);
    this._entries.push(entry);

    callback();
  }.bind(this));
};

Archiver.prototype._onStatQueueTask = function(task, callback) {
  if (this._state.finalizing || this._state.finalized || this._state.aborted) {
    callback();
    return;
  }

  fs.stat(task.filepath, function(err, stats) {
    if (err) {
      this.emit('error', err);
      callback();
      return;
    }

    task = this._updateQueueTaskWithStats(task, stats);

    if (task.source !== null) {
      this._queue.push(task);
      callback();
    } else {
      this.emit('error', new Error('unsupported entry: ' + task.filepath));
      callback();
      return;
    }
  }.bind(this));
};

Archiver.prototype._updateQueueTaskWithStats = function(task, stats) {
  if (stats.isFile()) {
    task.data.type = 'file';
    task.data.sourceType = 'stream';
    task.source = util.lazyReadStream(task.filepath);
  } else if (stats.isDirectory() && this._moduleSupports('directory')) {
    task.data.name = util.trailingSlashIt(task.data.name);
    task.data.type = 'directory';
    task.data.sourcePath = util.trailingSlashIt(task.filepath);
    task.data.sourceType = 'buffer';
    task.source = new Buffer(0);
  } else {
    return task;
  }

  task.data = this._normalizeEntryData(task.data, stats);
  return task;
};

Archiver.prototype._pipeModuleOutput = function() {
  this._module.on('error', this._onModuleError.bind(this));
  this._module.pipe(this);

  this._state.modulePiped = true;
};

Archiver.prototype._processFile = function(source, data, callback) {
  this.emit('error', new Error('method not implemented'));
};

Archiver.prototype._transform = function(chunk, encoding, callback) {
  if (chunk) {
    this._pointer += chunk.length;
  }

  callback(null, chunk);
};

Archiver.prototype.abort = function() {
  if (this._state.aborted) {
    this.emit('error', new Error('abort: already aborted'));
    return this;
  }

  if (this._state.finalized) {
    this.emit('error', new Error('abort: already finalized'));
    return this;
  }

  this._abort();

  return this;
};

Archiver.prototype.append = function(source, data) {
  if (this._state.finalize || this._state.aborted) {
    this.emit('error', new Error('append: queue closed'));
    return this;
  }

  data = this._normalizeEntryData(data);

  if (typeof data.name !== 'string' || data.name.length === 0) {
    this.emit('error', new Error('append: entry name must be a non-empty string value'));
    return this;
  }

  if (data.type === 'directory' && !this._moduleSupports('directory')) {
    this.emit('error', new Error('append: entries of "directory" type not currently supported by this module'));
    return this;
  }

  source = util.normalizeInputSource(source);

  if (Buffer.isBuffer(source)) {
    data.sourceType = 'buffer';
  } else if (util.isStream(source)) {
    data.sourceType = 'stream';
  } else {
    this.emit('error', new Error('append: input source must be valid Stream or Buffer instance'));
    return this;
  }

  this._queue.push({
    data: data,
    source: source
  });

  return this;
};

Archiver.prototype.bulk = function(mappings) {
  if (this._state.finalize || this._state.aborted) {
    this.emit('error', new Error('bulk: queue closed'));
    return this;
  }

  if (!Array.isArray(mappings)) {
    mappings = [mappings];
  }

  var self = this;
  var files = util.file.normalizeFilesArray(mappings);

  files.forEach(function(file){
    var isExpandedPair = file.orig.expand || false;
    var fileData = file.data || {};

    file.src.forEach(function(filepath) {
      var data = util._.extend({}, fileData);
      data.name = isExpandedPair ? util.unixifyPath(file.dest) : util.unixifyPath(file.dest || '', filepath);

      if (data.name === '.') {
        return;
      }

      self._append(filepath, data);
    });
  });

  return this;
};

Archiver.prototype.file = function(filepath, data) {
  if (this._state.finalize || this._state.aborted) {
    this.emit('error', new Error('file: queue closed'));
    return this;
  }

  if (typeof filepath !== 'string' || filepath.length === 0) {
    this.emit('error', new Error('file: filepath must be a non-empty string value'));
    return this;
  }

  this._append(filepath, data);

  return this;
};

Archiver.prototype.finalize = function() {
  if (this._state.aborted) {
    this.emit('error', new Error('finalize: archive was aborted'));
    return this;
  }

  if (this._state.finalize) {
    this.emit('error', new Error('finalize: archive already finalizing'));
    return this;
  }

  this._state.finalize = true;

  if (this._queue.idle() && this._statQueue.idle()) {
    this._moduleFinalize();
  }

  return this;
};

Archiver.prototype.setModule = function(module) {
  if (this._state.aborted) {
    this.emit('error', new Error('module: archive was aborted'));
    return this;
  }

  if (this._state.modulePiped) {
    this.emit('error', new Error('module: module already set'));
    return;
  }

  this._module = module;
  this._pipeModuleOutput();
};

Archiver.prototype.pointer = function() {
  return this._pointer;
};<|endoftext|>


<commit_before>/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
const Package = require('dgeni').Package;

const basePackage = require('../angular-base-package');
const typeScriptPackage = require('dgeni-packages/typescript');
const { API_SOURCE_PATH, API_TEMPLATES_PATH, requireFolder } = require('../config');

module.exports = new Package('angular-api', [basePackage, typeScriptPackage])

  // Register the processors
  .processor(require('./processors/splitDescription'))
  .processor(require('./processors/convertPrivateClassesToInterfaces'))
  .processor(require('./processors/generateApiListDoc'))
  .processor(require('./processors/addNotYetDocumentedProperty'))
  .processor(require('./processors/mergeDecoratorDocs'))
  .processor(require('./processors/extractDecoratedClasses'))
  .processor(require('./processors/extractPipeParams'))
  .processor(require('./processors/matchUpDirectiveDecorators'))
  .processor(require('./processors/addMetadataAliases'))
  .processor(require('./processors/computeApiBreadCrumbs'))
  .processor(require('./processors/filterContainedDocs'))
  .processor(require('./processors/processClassLikeMembers'))
  .processor(require('./processors/markBarredODocsAsPrivate'))
  .processor(require('./processors/filterPrivateDocs'))
  .processor(require('./processors/computeSearchTitle'))
  .processor(require('./processors/simplifyMemberAnchors'))
  .processor(require('./processors/computeStability'))
  .processor(require('./processors/removeInjectableConstructors'))
  .processor(require('./processors/processPackages'))
  .processor(require('./processors/processNgModuleDocs'))

  /**
   * These are the API doc types that will be rendered to actual files.
   * This is a super set of the exported docs, since we convert some classes to
   * more Angular specific API types, such as decorators and directives.
   */
  .factory(function API_DOC_TYPES_TO_RENDER(EXPORT_DOC_TYPES) {
    return EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe', 'package']);
  })

  /**
   * These are the doc types that are contained within other docs
   */
  .factory(function API_CONTAINED_DOC_TYPES() {
    return ['member', 'function-overload', 'get-accessor-info', 'set-accessor-info', 'parameter'];
  })

  /**
   * These are the doc types that are API docs, including ones that will be merged into container docs,
   * such as members and overloads.
   */
  .factory(function API_DOC_TYPES(API_DOC_TYPES_TO_RENDER, API_CONTAINED_DOC_TYPES) {
    return API_DOC_TYPES_TO_RENDER.concat(API_CONTAINED_DOC_TYPES);
  })

  .factory(require('./readers/package-content'))

  // Where do we get the source files?
  .config(function(readTypeScriptModules, readFilesProcessor, collectExamples, tsParser, packageContentFileReader) {

    // Tell TypeScript how to load modules that start with with `@angular`
    tsParser.options.paths = { '@angular/*': [API_SOURCE_PATH + '/*'] };
    tsParser.options.baseUrl = '.';

    // API files are typescript
    readTypeScriptModules.basePath = API_SOURCE_PATH;
    readTypeScriptModules.ignoreExportsMatching = [/^[_ɵ]|^VERSION$/];
    readTypeScriptModules.hidePrivateMembers = true;

    // NOTE: This list shold be in sync with tools/public_api_guard/BUILD.bazel
    readTypeScriptModules.sourceFiles = [
      'animations/index.ts',
      'animations/browser/index.ts',
      'animations/browser/testing/index.ts',
      'common/http/index.ts',
      'common/http/testing/index.ts',
      'common/index.ts',
      'common/testing/index.ts',
      'core/index.ts',
      'core/testing/index.ts',
      'elements/index.ts',
      'forms/index.ts',
      'http/index.ts',
      'http/testing/index.ts',
      'platform-browser/index.ts',
      'platform-browser/animations/index.ts',
      'platform-browser/testing/index.ts',
      'platform-browser-dynamic/index.ts',
      'platform-browser-dynamic/testing/index.ts',
      'platform-server/index.ts',
      'platform-server/testing/index.ts',
      'platform-webworker/index.ts',
      'platform-webworker-dynamic/index.ts',
      'router/index.ts',
      'router/testing/index.ts',
      'router/upgrade/index.ts',
      'service-worker/index.ts',
      'upgrade/index.ts',
      'upgrade/static/index.ts',
    ];

    readFilesProcessor.fileReaders.push(packageContentFileReader);

    // API Examples
    readFilesProcessor.sourceFiles = [
      {
        basePath: API_SOURCE_PATH,
        include: API_SOURCE_PATH + '/examples/**/*',
        fileReader: 'exampleFileReader'
      },
      {
        basePath: API_SOURCE_PATH,
        include: API_SOURCE_PATH + '/**/PACKAGE.md',
        fileReader: 'packageContentFileReader'
      }
    ];
    collectExamples.exampleFolders.push('examples');
  })

  // Configure jsdoc-style tag parsing
  .config(function(parseTagsProcessor, getInjectables, tsHost) {
    // Load up all the tag definitions in the tag-defs folder
    parseTagsProcessor.tagDefinitions =
        parseTagsProcessor.tagDefinitions.concat(getInjectables(requireFolder(__dirname, './tag-defs')));
    // We don't want license headers to be joined to the first API item's comment
    tsHost.concatMultipleLeadingComments = false;
  })

  .config(function(computeStability, splitDescription, addNotYetDocumentedProperty, API_DOC_TYPES_TO_RENDER, API_DOC_TYPES) {
    computeStability.docTypes = API_DOC_TYPES_TO_RENDER;
    // Only split the description on the API docs
    splitDescription.docTypes = API_DOC_TYPES.concat(['package-content']);
    addNotYetDocumentedProperty.docTypes = API_DOC_TYPES;
  })

  .config(function(mergeDecoratorDocs) {
    mergeDecoratorDocs.propertiesToMerge = [
      'shortDescription',
      'description',
      'security',
      'deprecated',
      'see',
      'usageNotes',
    ];
  })

  .config(function(checkContentRules, EXPORT_DOC_TYPES) {
    // Min length rules
    const createMinLengthRule = require('./content-rules/minLength');
    const paramRuleSet = checkContentRules.docTypeRules['parameter'] = checkContentRules.docTypeRules['parameter'] || {};
    const paramRules = paramRuleSet['name'] = paramRuleSet['name'] || [];
    paramRules.push(createMinLengthRule());

    // Heading rules
    const createNoMarkdownHeadingsRule = require('./content-rules/noMarkdownHeadings');
    const noMarkdownHeadings = createNoMarkdownHeadingsRule();
    const allowOnlyLevel3Headings = createNoMarkdownHeadingsRule(1, 2, '4,');
    const DOC_TYPES_TO_CHECK = EXPORT_DOC_TYPES.concat(['member', 'overload-info']);
    const PROPS_TO_CHECK = ['description', 'shortDescription'];

    DOC_TYPES_TO_CHECK.forEach(docType => {
      const ruleSet = checkContentRules.docTypeRules[docType] = checkContentRules.docTypeRules[docType] || {};
      PROPS_TO_CHECK.forEach(prop => {
        const rules = ruleSet[prop] = ruleSet[prop] || [];
        rules.push(noMarkdownHeadings);
      });
      const rules = ruleSet['usageNotes'] = ruleSet['usageNotes'] || [];
      rules.push(allowOnlyLevel3Headings);
    });
  })

  .config(function(filterContainedDocs, API_CONTAINED_DOC_TYPES) {
    filterContainedDocs.docTypes = API_CONTAINED_DOC_TYPES;
  })

  .config(function(checkContentRules, API_DOC_TYPES, API_CONTAINED_DOC_TYPES) {
    addMinLengthRules(checkContentRules);
    addHeadingRules(checkContentRules, API_DOC_TYPES);
    addAllowedPropertiesRules(checkContentRules, API_CONTAINED_DOC_TYPES);
  })

  .config(function(computePathsProcessor, EXPORT_DOC_TYPES, generateApiListDoc) {

    const API_SEGMENT = 'api';

    generateApiListDoc.outputFolder = API_SEGMENT;

    computePathsProcessor.pathTemplates.push({
      docTypes: ['package'],
      getPath: function computeModulePath(doc) {
        doc.moduleFolder = `${API_SEGMENT}/${doc.id.replace(/\/index$/, '')}`;
        return doc.moduleFolder;
      },
      outputPathTemplate: '${moduleFolder}.json'
    });
    computePathsProcessor.pathTemplates.push({
      docTypes: EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe']),
      pathTemplate: '${moduleDoc.moduleFolder}/${name}',
      outputPathTemplate: '${moduleDoc.moduleFolder}/${name}.json',
    });
  })

  .config(function(templateFinder) {
    // Where to find the templates for the API doc rendering
    templateFinder.templateFolders.unshift(API_TEMPLATES_PATH);
  })

  .config(function(convertToJsonProcessor, postProcessHtml, API_DOC_TYPES_TO_RENDER, API_DOC_TYPES, autoLinkCode) {
    convertToJsonProcessor.docTypes = convertToJsonProcessor.docTypes.concat(API_DOC_TYPES_TO_RENDER);
    postProcessHtml.docTypes = convertToJsonProcessor.docTypes.concat(API_DOC_TYPES_TO_RENDER);
    autoLinkCode.docTypes = API_DOC_TYPES;
    autoLinkCode.codeElements = ['code', 'code-example', 'code-pane'];
  });

function addMinLengthRules(checkContentRules) {
  const createMinLengthRule = require('./content-rules/minLength');
  const paramRuleSet = checkContentRules.docTypeRules['parameter'] = checkContentRules.docTypeRules['parameter'] || {};
  const paramRules = paramRuleSet['name'] = paramRuleSet['name'] || [];
  paramRules.push(createMinLengthRule());
}

function addHeadingRules(checkContentRules, API_DOC_TYPES) {
  const createNoMarkdownHeadingsRule = require('./content-rules/noMarkdownHeadings');
  const noMarkdownHeadings = createNoMarkdownHeadingsRule();
  const allowOnlyLevel3Headings = createNoMarkdownHeadingsRule(1, 2, '4,');

  API_DOC_TYPES.forEach(docType => {
    let rules;
    const ruleSet = checkContentRules.docTypeRules[docType] = checkContentRules.docTypeRules[docType] || {};

    rules = ruleSet['description'] = ruleSet['description'] || [];
    rules.push(noMarkdownHeadings);

    rules = ruleSet['shortDescription'] = ruleSet['shortDescription'] || [];
    rules.push(noMarkdownHeadings);

    rules = ruleSet['usageNotes'] = ruleSet['usageNotes'] || [];
    rules.push(allowOnlyLevel3Headings);
  });
}

function addAllowedPropertiesRules(checkContentRules, API_CONTAINED_DOC_TYPES) {
  API_CONTAINED_DOC_TYPES.forEach(docType => {
    const ruleSet = checkContentRules.docTypeRules[docType] = checkContentRules.docTypeRules[docType] || {};

    const rules = ruleSet['usageNotes'] = ruleSet['usageNotes'] || [];
    rules.push((doc, prop, value) =>
      value &&
      // methods are allowed to have usage notes
      !isMethod(doc) &&
      // options on decorators are allowed to ahve usage notes
      !(doc.containerDoc && doc.containerDoc.docType === 'decorator') &&
      `Invalid property: "${prop}" is not allowed on "${doc.docType}" docs.`);
  });
}

function isMethod(doc) {
  return doc.hasOwnProperty('parameters') && !doc.isGetAccessor && !doc.isSetAccessor;
}
<commit_msg>build(docs-infra): fail doc-gen if a content rule fails (#26039)<commit_after>/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
const Package = require('dgeni').Package;

const basePackage = require('../angular-base-package');
const typeScriptPackage = require('dgeni-packages/typescript');
const { API_SOURCE_PATH, API_TEMPLATES_PATH, requireFolder } = require('../config');

module.exports = new Package('angular-api', [basePackage, typeScriptPackage])

  // Register the processors
  .processor(require('./processors/splitDescription'))
  .processor(require('./processors/convertPrivateClassesToInterfaces'))
  .processor(require('./processors/generateApiListDoc'))
  .processor(require('./processors/addNotYetDocumentedProperty'))
  .processor(require('./processors/mergeDecoratorDocs'))
  .processor(require('./processors/extractDecoratedClasses'))
  .processor(require('./processors/extractPipeParams'))
  .processor(require('./processors/matchUpDirectiveDecorators'))
  .processor(require('./processors/addMetadataAliases'))
  .processor(require('./processors/computeApiBreadCrumbs'))
  .processor(require('./processors/filterContainedDocs'))
  .processor(require('./processors/processClassLikeMembers'))
  .processor(require('./processors/markBarredODocsAsPrivate'))
  .processor(require('./processors/filterPrivateDocs'))
  .processor(require('./processors/computeSearchTitle'))
  .processor(require('./processors/simplifyMemberAnchors'))
  .processor(require('./processors/computeStability'))
  .processor(require('./processors/removeInjectableConstructors'))
  .processor(require('./processors/processPackages'))
  .processor(require('./processors/processNgModuleDocs'))

  /**
   * These are the API doc types that will be rendered to actual files.
   * This is a super set of the exported docs, since we convert some classes to
   * more Angular specific API types, such as decorators and directives.
   */
  .factory(function API_DOC_TYPES_TO_RENDER(EXPORT_DOC_TYPES) {
    return EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe', 'package']);
  })

  /**
   * These are the doc types that are contained within other docs
   */
  .factory(function API_CONTAINED_DOC_TYPES() {
    return ['member', 'function-overload', 'get-accessor-info', 'set-accessor-info', 'parameter'];
  })

  /**
   * These are the doc types that are API docs, including ones that will be merged into container docs,
   * such as members and overloads.
   */
  .factory(function API_DOC_TYPES(API_DOC_TYPES_TO_RENDER, API_CONTAINED_DOC_TYPES) {
    return API_DOC_TYPES_TO_RENDER.concat(API_CONTAINED_DOC_TYPES);
  })

  .factory(require('./readers/package-content'))

  // Where do we get the source files?
  .config(function(readTypeScriptModules, readFilesProcessor, collectExamples, tsParser, packageContentFileReader) {

    // Tell TypeScript how to load modules that start with with `@angular`
    tsParser.options.paths = { '@angular/*': [API_SOURCE_PATH + '/*'] };
    tsParser.options.baseUrl = '.';

    // API files are typescript
    readTypeScriptModules.basePath = API_SOURCE_PATH;
    readTypeScriptModules.ignoreExportsMatching = [/^[_ɵ]|^VERSION$/];
    readTypeScriptModules.hidePrivateMembers = true;

    // NOTE: This list shold be in sync with tools/public_api_guard/BUILD.bazel
    readTypeScriptModules.sourceFiles = [
      'animations/index.ts',
      'animations/browser/index.ts',
      'animations/browser/testing/index.ts',
      'common/http/index.ts',
      'common/http/testing/index.ts',
      'common/index.ts',
      'common/testing/index.ts',
      'core/index.ts',
      'core/testing/index.ts',
      'elements/index.ts',
      'forms/index.ts',
      'http/index.ts',
      'http/testing/index.ts',
      'platform-browser/index.ts',
      'platform-browser/animations/index.ts',
      'platform-browser/testing/index.ts',
      'platform-browser-dynamic/index.ts',
      'platform-browser-dynamic/testing/index.ts',
      'platform-server/index.ts',
      'platform-server/testing/index.ts',
      'platform-webworker/index.ts',
      'platform-webworker-dynamic/index.ts',
      'router/index.ts',
      'router/testing/index.ts',
      'router/upgrade/index.ts',
      'service-worker/index.ts',
      'upgrade/index.ts',
      'upgrade/static/index.ts',
    ];

    readFilesProcessor.fileReaders.push(packageContentFileReader);

    // API Examples
    readFilesProcessor.sourceFiles = [
      {
        basePath: API_SOURCE_PATH,
        include: API_SOURCE_PATH + '/examples/**/*',
        fileReader: 'exampleFileReader'
      },
      {
        basePath: API_SOURCE_PATH,
        include: API_SOURCE_PATH + '/**/PACKAGE.md',
        fileReader: 'packageContentFileReader'
      }
    ];
    collectExamples.exampleFolders.push('examples');
  })

  // Configure jsdoc-style tag parsing
  .config(function(parseTagsProcessor, getInjectables, tsHost) {
    // Load up all the tag definitions in the tag-defs folder
    parseTagsProcessor.tagDefinitions =
        parseTagsProcessor.tagDefinitions.concat(getInjectables(requireFolder(__dirname, './tag-defs')));
    // We don't want license headers to be joined to the first API item's comment
    tsHost.concatMultipleLeadingComments = false;
  })

  .config(function(computeStability, splitDescription, addNotYetDocumentedProperty, API_DOC_TYPES_TO_RENDER, API_DOC_TYPES) {
    computeStability.docTypes = API_DOC_TYPES_TO_RENDER;
    // Only split the description on the API docs
    splitDescription.docTypes = API_DOC_TYPES.concat(['package-content']);
    addNotYetDocumentedProperty.docTypes = API_DOC_TYPES;
  })

  .config(function(mergeDecoratorDocs) {
    mergeDecoratorDocs.propertiesToMerge = [
      'shortDescription',
      'description',
      'security',
      'deprecated',
      'see',
      'usageNotes',
    ];
  })

  .config(function(checkContentRules, API_DOC_TYPES, API_CONTAINED_DOC_TYPES) {
    addMinLengthRules(checkContentRules);
    addHeadingRules(checkContentRules, API_DOC_TYPES);
    addAllowedPropertiesRules(checkContentRules, API_CONTAINED_DOC_TYPES);
    checkContentRules.failOnContentErrors = true;
  })

  .config(function(filterContainedDocs, API_CONTAINED_DOC_TYPES) {
    filterContainedDocs.docTypes = API_CONTAINED_DOC_TYPES;
  })

  .config(function(computePathsProcessor, EXPORT_DOC_TYPES, generateApiListDoc) {

    const API_SEGMENT = 'api';

    generateApiListDoc.outputFolder = API_SEGMENT;

    computePathsProcessor.pathTemplates.push({
      docTypes: ['package'],
      getPath: function computeModulePath(doc) {
        doc.moduleFolder = `${API_SEGMENT}/${doc.id.replace(/\/index$/, '')}`;
        return doc.moduleFolder;
      },
      outputPathTemplate: '${moduleFolder}.json'
    });
    computePathsProcessor.pathTemplates.push({
      docTypes: EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe']),
      pathTemplate: '${moduleDoc.moduleFolder}/${name}',
      outputPathTemplate: '${moduleDoc.moduleFolder}/${name}.json',
    });
  })

  .config(function(templateFinder) {
    // Where to find the templates for the API doc rendering
    templateFinder.templateFolders.unshift(API_TEMPLATES_PATH);
  })

  .config(function(convertToJsonProcessor, postProcessHtml, API_DOC_TYPES_TO_RENDER, API_DOC_TYPES, autoLinkCode) {
    convertToJsonProcessor.docTypes = convertToJsonProcessor.docTypes.concat(API_DOC_TYPES_TO_RENDER);
    postProcessHtml.docTypes = convertToJsonProcessor.docTypes.concat(API_DOC_TYPES_TO_RENDER);
    autoLinkCode.docTypes = API_DOC_TYPES;
    autoLinkCode.codeElements = ['code', 'code-example', 'code-pane'];
  });

function addMinLengthRules(checkContentRules) {
  const createMinLengthRule = require('./content-rules/minLength');
  const paramRuleSet = checkContentRules.docTypeRules['parameter'] = checkContentRules.docTypeRules['parameter'] || {};
  const paramRules = paramRuleSet['name'] = paramRuleSet['name'] || [];
  paramRules.push(createMinLengthRule());
}

function addHeadingRules(checkContentRules, API_DOC_TYPES) {
  const createNoMarkdownHeadingsRule = require('./content-rules/noMarkdownHeadings');
  const noMarkdownHeadings = createNoMarkdownHeadingsRule();
  const allowOnlyLevel3Headings = createNoMarkdownHeadingsRule(1, 2, '4,');

  API_DOC_TYPES.forEach(docType => {
    let rules;
    const ruleSet = checkContentRules.docTypeRules[docType] = checkContentRules.docTypeRules[docType] || {};

    rules = ruleSet['description'] = ruleSet['description'] || [];
    rules.push(noMarkdownHeadings);

    rules = ruleSet['shortDescription'] = ruleSet['shortDescription'] || [];
    rules.push(noMarkdownHeadings);

    rules = ruleSet['usageNotes'] = ruleSet['usageNotes'] || [];
    rules.push(allowOnlyLevel3Headings);
  });
}

function addAllowedPropertiesRules(checkContentRules, API_CONTAINED_DOC_TYPES) {
  API_CONTAINED_DOC_TYPES.forEach(docType => {
    const ruleSet = checkContentRules.docTypeRules[docType] = checkContentRules.docTypeRules[docType] || {};

    const rules = ruleSet['usageNotes'] = ruleSet['usageNotes'] || [];
    rules.push((doc, prop, value) =>
      value &&
      // methods are allowed to have usage notes
      !isMethod(doc) &&
      // options on decorators are allowed to ahve usage notes
      !(doc.containerDoc && doc.containerDoc.docType === 'decorator') &&
      `Invalid property: "${prop}" is not allowed on "${doc.docType}" docs.`);
  });
}

function isMethod(doc) {
  return doc.hasOwnProperty('parameters') && !doc.isGetAccessor && !doc.isSetAccessor;
}
<|endoftext|>


<commit_before>import {configure, Config} from '../src/aurelia-api';
import {Container} from 'aurelia-dependency-injection';

describe('aurelia-api', function() {
  describe('configure()', function() {
    it('Should call callback with a config function', function(done) {
      configure(aurelia(), function(config) {
        expect(config).toBe(Config);

        done();
      });
    });

    it('Should call callback with a config instance', function(done) {
      configure(aurelia(new Container()), function(config) {
        expect(config instanceof Config).toBe(true);

        done();
      });
    });
  });
});

function aurelia(container) {
  return {
    container: container || {
      get: function returnVal(val) {
        return val;
      }
    }
  };
}
<commit_msg>chore(test): add test to detect export changes<commit_after>import {configure, Config, Rest, Endpoint} from '../src/aurelia-api';
import {Container} from 'aurelia-dependency-injection';

describe('aurelia-api', function() {
  describe('export', function() {
    it('Should export configure', function() {
      expect(configure).toBeDefined();
    });

    it('Should export Config', function() {
      expect(Config).toBeDefined();
    });

    it('Should export Rest', function() {
      expect(Rest).toBeDefined();
    });

    it('Should export Endpoint', function() {
      expect(Endpoint).toBeDefined();
    });
  });

  describe('configure()', function() {
    it('Should call callback with a config function', function(done) {
      configure(aurelia(), function(config) {
        expect(config).toBe(Config);

        done();
      });
    });

    it('Should call callback with a config instance', function(done) {
      configure(aurelia(new Container()), function(config) {
        expect(config instanceof Config).toBe(true);

        done();
      });
    });
  });
});

function aurelia(container) {
  return {
    container: container || {
      get: function returnVal(val) {
        return val;
      }
    }
  };
}
<|endoftext|>