[
  {
    "path": ".github/workflows/main.yml",
    "content": "on:\n  issue_comment:\n    types: [created]\n  issues:\n    types: [opened]\n\njobs:\n  carbonate:\n    runs-on: ubuntu-latest\n    name: Generate beautiful images for code blocks present in issues\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Generate beautiful images for code blocks present in issues\n        uses: ./\n        id: carbonate\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          imgur-client-id: ${{ secrets.IMGUR_CLIENT_ID }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignoring the folder where I have the debug data, for easier reference during dev\ndata/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Mithun Kamath\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Carbonate\n\nJazz up the code blocks in your issues. Generate beautiful images for them to make it easier to follow. Meant to be used as a Github Action.\n\n**BEFORE**\n\n![](https://i.imgur.com/FzLtUjP.png)\n\n**AFTER**\n\n![](https://i.imgur.com/B29aF97.png)\n\n> First appeared at: https://stackoverflow.com/a/61269447/2104976\n\n## Features\n\nThe workflow of this action is as follows:\n\n- It extracts the code block for the issue description / comment and generates images for them\n- It then inserts the image at the code block\n- It also retains the original code block as a collapsed detail in the same issue / comment body\n\nAdditionally, it\n\n- Allows formatting the code using Prettier and controlling the styling of the images generated\n- Supports the following events:\n  - issue_comment:\n    - types: created\n  - issues:\n    - types: opened\n\n## Not supported (yet)\n\n- Generating images from multiple code blocks in the same issue description / comment\n- Generating images after the issue description / comment has been edited\n\n## Advantages of code images over code blocks\n\n- Easy to view and understand the image of the code v/s the code block text when using a mobile device. Why? Easier to scroll images v/s text.\n- Members will no longer have to rely on the issue reporters and commenters to format their code blocks correctly. Using the in built formatter, the code is always structured properly\n- Maintainers can style the code blocks to suit their project's language and guidelines and not put the onus of this on the issue reporter / commenter\n- [Carbon]((https://github.com/carbon-app/carbon)). Oh wow Carbon! It generates really beautiful images of code and it is aesthetically better to look at v/s plain code text\n\n## Pre-requisites\n\n[Register](https://api.imgur.com/oauth2/addclient) your application with Imgur to get a `client_id`. You will pass this as input to the action.\n\n## Usage\n\n```yaml\non:\n  issue_comment:\n    types: [created]\n  issues:\n    types: [opened]\n\njobs:\n  carbonate:\n    runs-on: ubuntu-latest\n    name: Generate beautiful images for code blocks present in issues\n    steps:\n      - name: Generate beautiful images for code blocks present in issues\n        uses: callmekatootie/carbonate@v1.0.2\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          imgur-client-id: ${{ secrets.IMGUR_CLIENT_ID }}\n```\n\nMore inputs are available. See below.\n\n### Inputs\n\n#### github-token\n\nThis is the environment variable [GITHUB_TOKEN](https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#about-the-github_token-secret).\n\n#### imgur-client-id\n\nThe `client_id` that you obtained after [registration](https://api.imgur.com/oauth2/addclient) with imgur. The action will be carrying out anonymous uploads. **Required**.\n\n#### use-prettier\n\nThe action can use [Prettier](https://prettier.io/) to format the code block before the image is generated. Set this input's value to the String `'true'` if you want to use it or to the String `'false'` otherwise. **Default value is 'true'**\n\n#### prettier-parser\n\nThis input is only read if `use-prettier` input is `'true'`. You will specify the [parser](https://prettier.io/docs/en/options.html#parser) that you want Prettier to use for formatting the code. **Default value is 'babel'**\n\n#### prettier-options\n\nThis input is only read if `use-prettier` input is `'true'`. You can specify the format [options](https://prettier.io/docs/en/options.html) to use with Prettier. **You need to pass the options object as a JSON string**. If none is passed, the action will fall back to Prettier's default format options.\n\n#### carbon-options\n\nYou can specify the Carbon image generation configuration options. See [this](https://github.com/petersolopov/carbonara#post-apicook) for a list of supported options. **You need to pass the options object as a JSON string**. If none is passed, the action will use the options defined in the `constants.js` file.\n\n## References\n\n- [Carbon](https://github.com/carbon-app/carbon) - Generate beautiful images for code\n- [Carbonara](https://github.com/petersolopov/carbonara) - Unofficial API for Carbon\n- [Imgur](https://apidocs.imgur.com/) - Generated images are stored in Imgur\n- [Prettier](https://prettier.io) - Opinionated Code Formatter\n"
  },
  {
    "path": "action.yml",
    "content": "---\nname: 'Carbonate'\ndescription: 'Generate beautiful images for code blocks present in issues'\ninputs:\n  github-token:\n    description: 'The GITHUB_TOKEN. Will read off the environment variable by default'\n    required: false\n    default: ${{ github.token }}\n  imgur-client-id:\n    description: 'All images are hosted on Imgur. Thus, provide your Imgur app client id'\n    required: true\n  use-prettier:\n    description: 'Should the code block be formatted using prettier? Pass String `true` or String `false`. Default is `true`'\n    required: false\n    default: 'true'\n  prettier-parser:\n    description: 'This is read only if `use-prettier` is set to `true`. This is the parser to use with Prettier. Default value is babel'\n    required: false\n    default: 'babel'\n  prettier-options:\n    description: 'This is read only if `use-prettier` is set to `true`. This is the Prettier options object, as a JSON string. Default values are the ones defined by Prettier itself. Check out their documentation'\n    required: false\n  carbon-options:\n    description: 'The Carbon options object, as a JSON string. See `https://github.com/petersolopov/carbonara#post-apicook` for a list of supported options. Ignores the `code` property though. For default values, please check out the constants.js file in the source code of this action'\n    required: false\nruns:\n  using: 'node12'\n  main: 'index.js'\nbranding:\n  icon: 'code'\n  color: 'blue'\n...\n"
  },
  {
    "path": "constants.js",
    "content": "const IMAGE_FILE_EXT = '.png'\n\nconst IMGUR_API_URL = 'https://api.imgur.com/3/image'\n\nconst CARBON_API_URL = 'https://carbonara-42.herokuapp.com/api/cook'\n\nconst CARBON_DEFAULT_SETTINGS = {\n  paddingVertical: '56px',\n  paddingHorizontal: '56px',\n  backgroundColor: 'rgba(74,144,226,1)',\n  dropShadow: true,\n  dropShadowOffsetY: '20px',\n  dropShadowBlurRadius: '68px',\n  theme: 'one-dark',\n  windowTheme: 'none',\n  language: 'auto',\n  fontFamily: 'Hack',\n  fontSize: '14px',\n  lineHeight: '143%',\n  windowControls: false,\n  widthAdjustment: true,\n  lineNumbers: false,\n  exportSize: '2x',\n  watermark: false\n}\n\nmodule.exports = {\n  IMAGE_FILE_EXT,\n  IMGUR_API_URL,\n  CARBON_API_URL,\n  CARBON_DEFAULT_SETTINGS\n}\n"
  },
  {
    "path": "index.js",
    "content": "const core = require('@actions/core')\nconst github = require('@actions/github')\nconst gcb = require('gfm-code-blocks')\nconst { default: Axios } = require('axios')\nconst fs = require('fs')\nconst { v4: uuidv4 } = require('uuid')\nconst FormData = require('form-data')\nconst path = require('path')\nconst util = require('util')\nconst prettier = require('prettier')\nconst {\n  IMAGE_FILE_EXT,\n  IMGUR_API_URL,\n  CARBON_API_URL,\n  CARBON_DEFAULT_SETTINGS\n} = require('./constants')\n\nconst unlink = util.promisify(fs.unlink)\n\n/**\n * Formats the code using prettier\n * @param {String} code The code block to format\n * @param {Object} options The Prettier options\n * @param {String} parser The Prettier parser\n */\nfunction formatCode (code, options, parser) {\n  try {\n    return prettier.format(code, { ...options, parser })\n  } catch (error) {\n    core.debug('An error occurred when using prettier')\n    core.debug(error)\n\n    // Return unformatted code\n    return code\n  }\n}\n\n/**\n * Generates the image using Carbon's API\n * @param {String} code The code to generate image for\n * @param {Object} options The {Unofficial} Carbon API options\n */\nasync function generateImage (code, options) {\n  const uuid = uuidv4()\n\n  core.info('Generating image from the code...')\n\n  try {\n    const res = await Axios.post(CARBON_API_URL, {\n      code,\n      ...CARBON_DEFAULT_SETTINGS,\n      ...options\n    }, {\n      responseType: 'stream'\n    })\n\n    const writeStream = fs.createWriteStream(path.join(__dirname, `${uuid}${IMAGE_FILE_EXT}`))\n\n    return new Promise((resolve, reject) => {\n      let error\n\n      res.data.pipe(writeStream)\n\n      writeStream.on('error', (err) => {\n        error = err\n        core.error('An error occurred downloading the generated image from Carbon')\n        core.debug(error)\n        writeStream.close()\n        reject(error)\n      })\n\n      writeStream.on('close', () => {\n        if (!error) {\n          // Return the file id\n          resolve(uuid)\n        }\n      })\n    })\n  } catch (error) {\n    core.error('An error occurred when trying to generate image for the code')\n    core.debug(error)\n\n    // Throw the error to abort the operation\n    throw error\n  }\n}\n\n/**\n * Uploads the image to Imgur\n * @param {String} imageId The uuid of the image\n */\nasync function uploadImage (imageId) {\n  const clientId = core.getInput('imgur-client-id')\n\n  const form = new FormData()\n  form.append('image', fs.createReadStream(path.join(__dirname, `${imageId}${IMAGE_FILE_EXT}`)))\n\n  core.info('Uploading image to imgur...')\n\n  try {\n    const res = await Axios.post(IMGUR_API_URL, form, {\n      headers: {\n        Authorization: `Client-ID ${clientId}`,\n        ...form.getHeaders()\n      }\n    })\n\n    return res.data.data.link\n  } catch (error) {\n    core.error('An error occurred when trying to upload image to imgur')\n    core.debug(error)\n\n    throw error\n  }\n}\n\n/**\n * Replaces the code block in a comment with the\n * corresponding image's url\n * @param {String} commentBody The entire comment body\n * @param {String} imageUrl The image to replace the code block with\n */\nfunction replaceCodeBlockWithImage (commentBody, imageUrl) {\n  // TODO - Support more than one code block\n  const codeblock = gcb(commentBody)[0]\n\n  // ! DO NOT change the formatting for this constant's value\n  // ! Intentionally set this way for the markdown to be correct during render\n  const replaceWith = `\\n<p align=\"center\"><img src=\"${imageUrl}\"/></p>\\n\\n---\\n\\n<details><summary>View raw code</summary>\n<p>\n\n${codeblock.block}\n\n</p></details>\\n\\n---\\n\\n`\n\n  return commentBody.replace(codeblock.block, replaceWith)\n}\n\n/**\n * Updates the comment\n * @param {Object} comment Details about the comment\n */\nasync function updateComment (comment) {\n  const githubToken = core.getInput('github-token')\n\n  const octokit = github.getOctokit(githubToken)\n\n  core.info('Updating comment...')\n\n  try {\n    await octokit.issues.updateComment(comment)\n  } catch (error) {\n    core.error('Error occurred updating the comment')\n    core.debug(error)\n\n    throw error\n  }\n}\n\n/**\n * Updates the issue\n * @param {Object} issue Details about the issue\n */\nasync function updateIssue (issue) {\n  const githubToken = core.getInput('github-token')\n\n  const octokit = github.getOctokit(githubToken)\n\n  core.info('Updating issue...')\n\n  try {\n    await octokit.issues.update(issue)\n  } catch (error) {\n    core.error('Error occurred updating the issue')\n    core.debug(error)\n\n    throw error\n  }\n}\n\n/**\n * Main function\n */\nasync function execute () {\n  let imageId\n  let code\n  let body\n  const usePrettier = core.getInput('use-prettier') === 'true'\n  const prettierParser = core.getInput('prettier-parser')\n  let prettierOptions = {}\n  let carbonOptions = {}\n\n  try {\n    prettierOptions = JSON.parse(core.getInput('prettier-options'))\n  } catch (error) {\n    core.info('Prettier options is not passed or not a valid JSON string. Falling back to default')\n  }\n\n  try {\n    carbonOptions = JSON.parse(core.getInput('carbon-options'))\n  } catch (error) {\n    core.info('Carbon options is not passed or not a valid JSON string. Falling back to default')\n  }\n\n  try {\n    const { eventName, payload } = github.context\n\n    core.debug(`Event name: ${eventName}`)\n    core.debug(`Payload action: ${payload.action}`)\n\n    if (eventName !== 'issues' && eventName !== 'issue_comment') {\n      core.info(`Unsupported event ${eventName}`)\n\n      return\n    }\n\n    core.debug('Is a supported event')\n\n    if ((eventName === 'issues' && payload.action !== 'opened') ||\n      (eventName === 'issue_comment' && payload.action !== 'created')) {\n      core.info(`Unsupported type ${payload.action} for event ${eventName}`)\n\n      return\n    }\n\n    core.debug('Is a supported type')\n\n    if (eventName === 'issues') {\n      body = payload.issue.body\n    } else {\n      body = payload.comment.body\n    }\n\n    core.debug(`Body is ${body}`)\n\n    core.debug('Before extracting code blocks')\n\n    const codeblocks = gcb(body)\n\n    core.debug('After extracting code blocks')\n\n    // TODO - Support more than one code block\n    if (codeblocks.length !== 1) {\n      core.info('No code block found or more than one code block found. Unsupported scenario for now. Quitting.')\n      return\n    }\n\n    core.debug(`Is prettier active: ${usePrettier}`)\n\n    if (usePrettier) {\n      code = formatCode(codeblocks[0].code, prettierOptions, prettierParser)\n    } else {\n      code = codeblocks[0].code\n    }\n\n    core.debug(`Extracted code block is ${code}`)\n\n    imageId = await generateImage(code, carbonOptions)\n\n    const imageUrl = await uploadImage(imageId)\n\n    core.debug(`The imgur url is ${imageUrl}`)\n\n    const updatedComment = replaceCodeBlockWithImage(body, imageUrl)\n\n    core.debug(`The updated comment is ${updateComment}`)\n\n    const updates = {\n      owner: payload.repository.owner.login,\n      repo: payload.repository.name,\n      body: updatedComment\n    }\n\n    if (eventName === 'issues') {\n      updates.issue_number = payload.issue.number\n\n      await updateIssue(updates)\n    } else {\n      updates.comment_id = payload.comment.id\n\n      await updateComment(updates)\n    }\n\n    core.info('Task completed')\n  } catch (error) {\n    core.setFailed(error.message)\n  } finally {\n    core.startGroup('View event payload')\n    core.debug(JSON.stringify(github, null, 4))\n    core.endGroup()\n    // Cleanup\n    if (imageId) {\n      await unlink(path.resolve(__dirname, `${imageId}${IMAGE_FILE_EXT}`))\n    }\n  }\n}\n\nexecute()\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"carbonate\",\n  \"version\": \"1.0.1\",\n  \"description\": \"Github action that generates beautiful images for code blocks present in issues\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"lint\": \"standard index.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/callmekatootie/carbonate.git\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/callmekatootie/carbonate/issues\"\n  },\n  \"homepage\": \"https://github.com/callmekatootie/carbonate#readme\",\n  \"dependencies\": {\n    \"@actions/core\": \"^1.2.4\",\n    \"@actions/github\": \"^4.0.0\",\n    \"axios\": \"^0.19.2\",\n    \"form-data\": \"^3.0.0\",\n    \"gfm-code-blocks\": \"^1.0.0\",\n    \"prettier\": \"^2.0.5\",\n    \"uuid\": \"^8.3.0\"\n  },\n  \"devDependencies\": {\n    \"standard\": \"^14.3.4\"\n  }\n}\n"
  }
]