Repository: callmekatootie/carbonate Branch: master Commit: 76eddf4bd2b0 Files: 8 Total size: 16.0 KB Directory structure: gitextract_o0qpgg0z/ ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── action.yml ├── constants.js ├── index.js └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/main.yml ================================================ on: issue_comment: types: [created] issues: types: [opened] jobs: carbonate: runs-on: ubuntu-latest name: Generate beautiful images for code blocks present in issues steps: - name: Checkout uses: actions/checkout@v2 - name: Generate beautiful images for code blocks present in issues uses: ./ id: carbonate with: github-token: ${{ secrets.GITHUB_TOKEN }} imgur-client-id: ${{ secrets.IMGUR_CLIENT_ID }} ================================================ FILE: .gitignore ================================================ # Ignoring the folder where I have the debug data, for easier reference during dev data/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Mithun Kamath Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Carbonate Jazz 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. **BEFORE** ![](https://i.imgur.com/FzLtUjP.png) **AFTER** ![](https://i.imgur.com/B29aF97.png) > First appeared at: https://stackoverflow.com/a/61269447/2104976 ## Features The workflow of this action is as follows: - It extracts the code block for the issue description / comment and generates images for them - It then inserts the image at the code block - It also retains the original code block as a collapsed detail in the same issue / comment body Additionally, it - Allows formatting the code using Prettier and controlling the styling of the images generated - Supports the following events: - issue_comment: - types: created - issues: - types: opened ## Not supported (yet) - Generating images from multiple code blocks in the same issue description / comment - Generating images after the issue description / comment has been edited ## Advantages of code images over code blocks - 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. - 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 - 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 - [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 ## Pre-requisites [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. ## Usage ```yaml on: issue_comment: types: [created] issues: types: [opened] jobs: carbonate: runs-on: ubuntu-latest name: Generate beautiful images for code blocks present in issues steps: - name: Generate beautiful images for code blocks present in issues uses: callmekatootie/carbonate@v1.0.2 with: github-token: ${{ secrets.GITHUB_TOKEN }} imgur-client-id: ${{ secrets.IMGUR_CLIENT_ID }} ``` More inputs are available. See below. ### Inputs #### github-token This 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). #### imgur-client-id The `client_id` that you obtained after [registration](https://api.imgur.com/oauth2/addclient) with imgur. The action will be carrying out anonymous uploads. **Required**. #### use-prettier The 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'** #### prettier-parser This 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'** #### prettier-options This 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. #### carbon-options You 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. ## References - [Carbon](https://github.com/carbon-app/carbon) - Generate beautiful images for code - [Carbonara](https://github.com/petersolopov/carbonara) - Unofficial API for Carbon - [Imgur](https://apidocs.imgur.com/) - Generated images are stored in Imgur - [Prettier](https://prettier.io) - Opinionated Code Formatter ================================================ FILE: action.yml ================================================ --- name: 'Carbonate' description: 'Generate beautiful images for code blocks present in issues' inputs: github-token: description: 'The GITHUB_TOKEN. Will read off the environment variable by default' required: false default: ${{ github.token }} imgur-client-id: description: 'All images are hosted on Imgur. Thus, provide your Imgur app client id' required: true use-prettier: description: 'Should the code block be formatted using prettier? Pass String `true` or String `false`. Default is `true`' required: false default: 'true' prettier-parser: description: 'This is read only if `use-prettier` is set to `true`. This is the parser to use with Prettier. Default value is babel' required: false default: 'babel' prettier-options: 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' required: false carbon-options: 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' required: false runs: using: 'node12' main: 'index.js' branding: icon: 'code' color: 'blue' ... ================================================ FILE: constants.js ================================================ const IMAGE_FILE_EXT = '.png' const IMGUR_API_URL = 'https://api.imgur.com/3/image' const CARBON_API_URL = 'https://carbonara-42.herokuapp.com/api/cook' const CARBON_DEFAULT_SETTINGS = { paddingVertical: '56px', paddingHorizontal: '56px', backgroundColor: 'rgba(74,144,226,1)', dropShadow: true, dropShadowOffsetY: '20px', dropShadowBlurRadius: '68px', theme: 'one-dark', windowTheme: 'none', language: 'auto', fontFamily: 'Hack', fontSize: '14px', lineHeight: '143%', windowControls: false, widthAdjustment: true, lineNumbers: false, exportSize: '2x', watermark: false } module.exports = { IMAGE_FILE_EXT, IMGUR_API_URL, CARBON_API_URL, CARBON_DEFAULT_SETTINGS } ================================================ FILE: index.js ================================================ const core = require('@actions/core') const github = require('@actions/github') const gcb = require('gfm-code-blocks') const { default: Axios } = require('axios') const fs = require('fs') const { v4: uuidv4 } = require('uuid') const FormData = require('form-data') const path = require('path') const util = require('util') const prettier = require('prettier') const { IMAGE_FILE_EXT, IMGUR_API_URL, CARBON_API_URL, CARBON_DEFAULT_SETTINGS } = require('./constants') const unlink = util.promisify(fs.unlink) /** * Formats the code using prettier * @param {String} code The code block to format * @param {Object} options The Prettier options * @param {String} parser The Prettier parser */ function formatCode (code, options, parser) { try { return prettier.format(code, { ...options, parser }) } catch (error) { core.debug('An error occurred when using prettier') core.debug(error) // Return unformatted code return code } } /** * Generates the image using Carbon's API * @param {String} code The code to generate image for * @param {Object} options The {Unofficial} Carbon API options */ async function generateImage (code, options) { const uuid = uuidv4() core.info('Generating image from the code...') try { const res = await Axios.post(CARBON_API_URL, { code, ...CARBON_DEFAULT_SETTINGS, ...options }, { responseType: 'stream' }) const writeStream = fs.createWriteStream(path.join(__dirname, `${uuid}${IMAGE_FILE_EXT}`)) return new Promise((resolve, reject) => { let error res.data.pipe(writeStream) writeStream.on('error', (err) => { error = err core.error('An error occurred downloading the generated image from Carbon') core.debug(error) writeStream.close() reject(error) }) writeStream.on('close', () => { if (!error) { // Return the file id resolve(uuid) } }) }) } catch (error) { core.error('An error occurred when trying to generate image for the code') core.debug(error) // Throw the error to abort the operation throw error } } /** * Uploads the image to Imgur * @param {String} imageId The uuid of the image */ async function uploadImage (imageId) { const clientId = core.getInput('imgur-client-id') const form = new FormData() form.append('image', fs.createReadStream(path.join(__dirname, `${imageId}${IMAGE_FILE_EXT}`))) core.info('Uploading image to imgur...') try { const res = await Axios.post(IMGUR_API_URL, form, { headers: { Authorization: `Client-ID ${clientId}`, ...form.getHeaders() } }) return res.data.data.link } catch (error) { core.error('An error occurred when trying to upload image to imgur') core.debug(error) throw error } } /** * Replaces the code block in a comment with the * corresponding image's url * @param {String} commentBody The entire comment body * @param {String} imageUrl The image to replace the code block with */ function replaceCodeBlockWithImage (commentBody, imageUrl) { // TODO - Support more than one code block const codeblock = gcb(commentBody)[0] // ! DO NOT change the formatting for this constant's value // ! Intentionally set this way for the markdown to be correct during render const replaceWith = `\n

\n\n---\n\n
View raw code

${codeblock.block}

\n\n---\n\n` return commentBody.replace(codeblock.block, replaceWith) } /** * Updates the comment * @param {Object} comment Details about the comment */ async function updateComment (comment) { const githubToken = core.getInput('github-token') const octokit = github.getOctokit(githubToken) core.info('Updating comment...') try { await octokit.issues.updateComment(comment) } catch (error) { core.error('Error occurred updating the comment') core.debug(error) throw error } } /** * Updates the issue * @param {Object} issue Details about the issue */ async function updateIssue (issue) { const githubToken = core.getInput('github-token') const octokit = github.getOctokit(githubToken) core.info('Updating issue...') try { await octokit.issues.update(issue) } catch (error) { core.error('Error occurred updating the issue') core.debug(error) throw error } } /** * Main function */ async function execute () { let imageId let code let body const usePrettier = core.getInput('use-prettier') === 'true' const prettierParser = core.getInput('prettier-parser') let prettierOptions = {} let carbonOptions = {} try { prettierOptions = JSON.parse(core.getInput('prettier-options')) } catch (error) { core.info('Prettier options is not passed or not a valid JSON string. Falling back to default') } try { carbonOptions = JSON.parse(core.getInput('carbon-options')) } catch (error) { core.info('Carbon options is not passed or not a valid JSON string. Falling back to default') } try { const { eventName, payload } = github.context core.debug(`Event name: ${eventName}`) core.debug(`Payload action: ${payload.action}`) if (eventName !== 'issues' && eventName !== 'issue_comment') { core.info(`Unsupported event ${eventName}`) return } core.debug('Is a supported event') if ((eventName === 'issues' && payload.action !== 'opened') || (eventName === 'issue_comment' && payload.action !== 'created')) { core.info(`Unsupported type ${payload.action} for event ${eventName}`) return } core.debug('Is a supported type') if (eventName === 'issues') { body = payload.issue.body } else { body = payload.comment.body } core.debug(`Body is ${body}`) core.debug('Before extracting code blocks') const codeblocks = gcb(body) core.debug('After extracting code blocks') // TODO - Support more than one code block if (codeblocks.length !== 1) { core.info('No code block found or more than one code block found. Unsupported scenario for now. Quitting.') return } core.debug(`Is prettier active: ${usePrettier}`) if (usePrettier) { code = formatCode(codeblocks[0].code, prettierOptions, prettierParser) } else { code = codeblocks[0].code } core.debug(`Extracted code block is ${code}`) imageId = await generateImage(code, carbonOptions) const imageUrl = await uploadImage(imageId) core.debug(`The imgur url is ${imageUrl}`) const updatedComment = replaceCodeBlockWithImage(body, imageUrl) core.debug(`The updated comment is ${updateComment}`) const updates = { owner: payload.repository.owner.login, repo: payload.repository.name, body: updatedComment } if (eventName === 'issues') { updates.issue_number = payload.issue.number await updateIssue(updates) } else { updates.comment_id = payload.comment.id await updateComment(updates) } core.info('Task completed') } catch (error) { core.setFailed(error.message) } finally { core.startGroup('View event payload') core.debug(JSON.stringify(github, null, 4)) core.endGroup() // Cleanup if (imageId) { await unlink(path.resolve(__dirname, `${imageId}${IMAGE_FILE_EXT}`)) } } } execute() ================================================ FILE: package.json ================================================ { "name": "carbonate", "version": "1.0.1", "description": "Github action that generates beautiful images for code blocks present in issues", "main": "index.js", "scripts": { "lint": "standard index.js" }, "repository": { "type": "git", "url": "git+https://github.com/callmekatootie/carbonate.git" }, "keywords": [], "author": "", "license": "MIT", "bugs": { "url": "https://github.com/callmekatootie/carbonate/issues" }, "homepage": "https://github.com/callmekatootie/carbonate#readme", "dependencies": { "@actions/core": "^1.2.4", "@actions/github": "^4.0.0", "axios": "^0.19.2", "form-data": "^3.0.0", "gfm-code-blocks": "^1.0.0", "prettier": "^2.0.5", "uuid": "^8.3.0" }, "devDependencies": { "standard": "^14.3.4" } }