[
  {
    "path": ".gitignore",
    "content": "node_modules/\ndist/\npackage-lock.json\nyarn.lock"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\n\n# Tells Travis to use the latest Node version \n# It defaults to an old one otherwise\nnode_js: node\n\n# By default Travis will run \"npm install\" and \"npm test\" for node projects\n# So we don't need to specify them here"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Chris Lewis\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": "# prettyplan [![Build Status](https://travis-ci.com/chrislewisdev/prettyplan.svg?branch=master)](https://travis-ci.com/chrislewisdev/prettyplan)\n\nPrettyplan ([available online here](https://prettyplan.chrislewisdev.com/)) is a small tool to help you view large Terraform plans with ease. By pasting in your plan output, it will be formatted for:\n\n - Expandable/collapsible sections to help you see your plan at a high level and in detail\n - Tabular layout for easy comparison of old/new values\n - Better display formatting of multi-line strings (such as JSON documents)\n \n## Terraform Version Compatibility\n\nPrettyplan was written to work on Terraform plans from 0.11 and earlier. In 0.12, the plan output was significantly changed, addressing many of the pain points that Prettyplan addresses; for this reason, there are no current plans to update Prettyplan to work with 0.12. In my case, Prettyplan was made unnecessary by Terraform's improvements.\n\nContributions are still welcome if anyone would like to upgrade the code to handle plans from 0.12 onwards.\n \n## Contributing\n\nYou're welcome to submit ideas/bugs (via the Issues section) or improvements (via Pull Requests)! \n\nThe code has all been converted from JavaScript to TypeScript and is built by webpack. To work on the project locally, you should be able to get everything up and running just by having `npm`, running `npm install` and then `npm run serve` which will open up Prettyplan in your default browser, ready for any changes you make to the source files.\n\nYou can also run `npm run build` to build the project without a dev server.\n \n### Tests\n\nTests are being run on every commit and Pull Request via Travis, but if you want to run them locally, you'll need to have `npm` on your PC, and run `npm install` followed by `npm test` in the repository.\n\n## Deployment\n\nThe project is deployed to Netlify; a slightly older version of the code is also served on GitHub pages which used to be the main way to access Prettyplan.\n\n## Will this steal sensitive data from my Terraform plans?\n\nNo. All the parsing/formatting is done directly in your browser, no data is sent to or from another service.\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n    \"roots\": [\"tests\"],\n    \"transform\": {\n        \"\\\\.ts$\": \"ts-jest\"\n    },\n    \"testRegex\": \"\\\\.test\\\\.ts$\",\n    \"moduleFileExtensions\": [\"ts\", \"js\"]\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"prettyplan\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Formatting tool for terraform plan output\",\n  \"author\": \"Chris Lewis\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/chrislewisdev/prettyplan\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"webpack\",\n    \"serve\": \"webpack-dev-server --open\"\n  },\n  \"devDependencies\": {\n    \"@types/jest\": \"^24.0.0\",\n    \"copy-webpack-plugin\": \"^4.6.0\",\n    \"html-webpack-plugin\": \"^3.2.0\",\n    \"jest\": \"^23.6.0\",\n    \"ts-jest\": \"^23.10.5\",\n    \"ts-loader\": \"^5.3.3\",\n    \"typescript\": \"^3.3.3\",\n    \"webpack\": \"^4.29.3\",\n    \"webpack-cli\": \"^3.2.3\",\n    \"webpack-dev-server\": \"^3.1.14\"\n  }\n}\n"
  },
  {
    "path": "src/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset=\"utf-8\" />\n    <title>prettyplan</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"stylesheet\" href=\"style.css\" />\n</head>\n\n<body>\n    <div class=\"stripe\"></div>\n    <div id=\"release-notification\" class=\"hidden\"></div>\n    <div id=\"branding\">\n        Source on <a href=\"https://github.com/chrislewisdev/prettyplan\">GitHub</a><br />\n        By <a href=\"https://twitter.com/chrislewisdev\">Chris Lewis</a><br />\n        <button class=\"text-button\" onclick=\"showReleaseNotes()\">Release Notes</button><br />\n        <a href=\"https://github.com/chrislewisdev/prettyplan-cli\">CLI version</a><br />\n   </div>\n    <div class=\"container\">\n        <h1>prettyplan</h1>\n        <p>Just paste in your output from terraform plan (or use the provided example), and hit Prettify!</p>\n        <p>Prettyplan does not support plans from Terraform 0.12+. For more info, <a href=\"https://github.com/chrislewisdev/prettyplan\">see the project's readme</a>.</p>\n        <textarea id=\"terraform-plan\">\n            Refreshing Terraform state in-memory prior to plan...\n            The refreshed state will be used to calculate this plan, but will not be\n            persisted to local or remote state storage.\n            \n            aws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)\n            aws_iam_role.service_role: Refreshing state... (ID: SampleApp)\n            aws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)\n            aws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)\n            aws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)\n            null_resource.promote_images: Refreshing state... (ID: 1236159896537553123)\n            aws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)\n            aws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)\n            aws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)\n            \n            ------------------------------------------------------------------------\n            \n            An execution plan has been generated and is shown below.\n            Resource actions are indicated with the following symbols:\n                ~ update in-place\n            -/+ destroy and then create replacement\n                &lt;= read (data resources)\n            \n            Terraform will perform the following actions:\n            \n                &lt;= data.external.ecr_image_digests\n                    id:                       &lt;computed&gt;\n                    program.#:                \"1\"\n                    program.0:                \"extract-image-digests\"\n                    result.%:                 &lt;computed&gt;\n            \n                ~ aws_ecs_service.sample_app\n                    task_definition:          \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\" =&gt; \"$&#123; aws_ecs_task_definition.sample_app.arn &#125;\"\n            \n            -/+ aws_ecs_task_definition.sample_app (new resource required)\n                    id:                       \"sample-app\" =&gt; &lt;computed&gt; (forces new resource)\n                    arn:                      \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\" =&gt; &lt;computed&gt;\n                    container_definitions:    \"[&#123;\\\"cpu\\\":1,\\\"environment\\\":[],\\\"essential\\\":true,\\\"image\\\":\\\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\\\",\\\"logConfiguration\\\":&#123;\\\"logDriver\\\":\\\"awslogs\\\",\\\"options\\\":&#123;\\\"awslogs-group\\\":\\\"sample-app\\\",\\\"awslogs-region\\\":\\\"us-east-1\\\",\\\"awslogs-stream-prefix\\\":\\\"sample-app\\\"&#125;&#125;,\\\"memory\\\":256,\\\"mountPoints\\\":[],\\\"name\\\":\\\"sample-app\\\",\\\"portMappings\\\":[&#123;\\\"containerPort\\\":8443,\\\"hostPort\\\":0,\\\"protocol\\\":\\\"tcp\\\"&#125;],\\\"volumesFrom\\\":[]&#125;]\" =&gt; \"[\\n  &#123;\\n    \\\"name\\\": \\\"sample-app\\\",\\n    \\\"image\\\": \\\"$&#123; aws_ecr_repository.sample_app.repository_url &#125;@$&#123; data.external.ecr_image_digests.result[\\\"sample-app\\\"] &#125;\\\",\\n    \\\"cpu\\\": 1,\\n    \\\"memory\\\": 256,\\n    \\\"essential\\\": true,\\n    \\\"logConfiguration\\\": &#123;\\n      \\\"logDriver\\\": \\\"awslogs\\\",\\n      \\\"options\\\": &#123;\\n        \\\"awslogs-group\\\": \\\"$&#123; aws_cloudwatch_log_group.sample_app.name &#125;\\\",\\n        \\\"awslogs-region\\\": \\\"$&#123; var.target_aws_region &#125;\\\",\\n        \\\"awslogs-stream-prefix\\\": \\\"sample-app\\\"\\n      &#125;\\n    &#125;,\\n    \\\"portMappings\\\": [\\n      &#123;\\n        \\\"containerPort\\\": 8443,\\n        \\\"hostPort\\\": 0\\n      &#125;\\n    ]\\n  &#125;\\n]\\n\" (forces new resource)\n                    family:                   \"sample-app\" =&gt; \"sample-app\"\n                    network_mode:             \"\" =&gt; &lt;computed&gt;\n                    revision:                 \"186\" =&gt; &lt;computed&gt;\n                    task_role_arn:            \"arn:aws:iam::123123123123:role/SampleApp\" =&gt; \"arn:aws:iam::123123123123:role/SampleApp\"\n            \n            -/+ null_resource.promote_images (new resource required)\n                    id:                       \"1236159896537553123\" =&gt; &lt;computed&gt; (forces new resource)\n                    triggers.%:               \"1\" =&gt; \"1\"\n                    triggers.deploy_job_hash: \"6c37ac7175bdf35e24a2f2755addd238\" =&gt; \"1a0bd86fc5831ee66858f2e159efa547\" (forces new resource)\n            \n            \n            Plan: 2 to add, 1 to change, 2 to destroy.\n            \n            ------------------------------------------------------------------------\n            \n            This plan was saved to: terraform.plan\n            \n            To perform exactly these actions, run the following command to apply:\n                terraform apply \"terraform.plan\"\n                                        </textarea><br />\n        <button onclick=\"runPrettyplan()\">Prettify it!</button>\n        <div id=\"parsing-error-message\" class=\"hidden\">That doesn't look like a Terraform plan. Did you copy the entire output\n            (without colouring) from the plan command?</div>\n        <div id=\"prettyplan\" class=\"prettyplan hidden\">\n            <ul id=\"errors\" class=\"errors\"></ul>\n            <ul id=\"warnings\" class=\"warnings\"></ul>\n            <button class=\"expand-all\" onclick=\"expandAll()\">Expand all</button>\n            <button class=\"collapse-all hidden\" onclick=\"collapseAll()\">Collapse all</button>\n            <ul id=\"actions\" class=\"actions\"></ul>\n        </div>\n    </div>\n</body>\n\n</html>"
  },
  {
    "path": "src/style.css",
    "content": "body {\n    font-family: Arial, Helvetica, sans-serif;\n    text-rendering: optimizeLegibility;\n    background: #ECF7FE;\n    color: #000000c0;\n    font-size: 15px;\n    margin: 0;\n}\n@keyframes fade-in {\n    0% {\n        opacity: 0;\n    }\n    100% {\n        opacity: 1;\n    }\n}\n\n.stripe {\n    width: 100%;\n    height: 5px;\n    background: #5C4CE4;\n    animation-name: wipe-in;\n    animation-duration: 1s;\n}\n@keyframes wipe-in {\n    0% {\n        width: 0%;\n    }\n    100% {\n        width: 100%;\n    }\n}\n\n#release-notification {\n    background: #5C4CE4;\n    color: white;\n    font-weight: bold;\n    text-align: center;\n    overflow: hidden;\n    padding: 10px 0 15px 0;\n    height: 20px;\n    animation-name: notification-pop-in;\n    animation-duration: 2s;\n}\n#release-notification a {\n    color: white;\n}\n#release-notification.dismissed {\n    animation-name: notification-pop-out;\n    animation-duration: .5s;\n    height: 0;\n    padding: 0;\n}\n@keyframes notification-pop-in {\n    0% {\n        height: 0;\n        padding: 0;\n    }\n    50% {\n        height: 0;\n        padding: 0;\n    }\n}\n@keyframes notification-pop-out {\n    0% {\n        height: 20px;\n        padding: 10px 0 15px 0;\n    }\n    100% {\n        height: 0;\n        padding: 0;\n    }\n}\n\n#modal-container {\n    animation-name: fade-in;\n    animation-duration: .2s;\n}\n.modal-pane {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: #ffffffe6;\n    z-index: 10; \n}\n.modal-content {\n    position: fixed;\n    width: 60%;\n    height: 60%;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    background: #ffffff;\n    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);\n    z-index: 20;\n}\n.modal-close {\n    position: absolute;\n    right: 0;\n    padding: 10px;\n}\n.modal-close button.text-button {\n    color: #4526AC;\n    text-decoration: none;\n    font-weight: normal;\n}\n.release-notes {\n    max-width: 80%;\n    margin: 0 auto 0 auto;\n    overflow-y: auto;\n    max-height: 100%;\n}\n\n#branding {\n    float: right;\n    padding-top: 10px;\n    padding-right: 10px;\n    font-size: 10px;\n    color: #4526AC;\n    text-align: right;\n}\n#branding a {\n    color: #4526AC;\n}\n\n.container {\n    margin: 10px 10px 0 10px;\n    animation-name: fade-in;\n    animation-duration: 1s;\n}\n@media only screen and (min-width: 600px) {\n    .container {\n        max-width: 80%;\n        margin-left: auto;\n        margin-right: auto;\n    }\n}\n\nh1, h2 {\n    text-align: center;\n    color: #4526AC;\n}\n\n#terraform-plan {\n    width: 100%;\n    min-height: 300px;\n    border: none;\n    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);\n    padding: 10px;\n    margin-bottom: 10px;\n    resize: none;\n    background: #ffffffe6;\n}\n\nbutton {\n    font-size: 18px;\n    background: #5C4CE4;\n    color: #fff;\n    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);\n    border: none;\n    border-radius: 2px;\n    width: 170px;\n    height: 40px;\n}\nbutton:hover {\n    background: #6567EA;\n    cursor: pointer;\n}\nbutton:active {\n    background: #5037CA;\n}\nbutton.text-button {\n    background: none;\n    box-shadow: none;\n    border-radius: 0;\n    width: auto;\n    height: auto;\n    text-decoration: underline;\n    font-size: inherit;\n    font-weight: inherit;\n    font-family: Arial, Helvetica, sans-serif; \n    color: inherit;\n    text-align: inherit;\n    padding: 0;\n}\n\n#parsing-error-message {\n    background-color: #ffffff;\n    padding: 10px;\n    color: #000000c0;\n    margin: 4px;\n    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);\n    font-weight: bold;\n    border-left: 2px solid red;\n    animation-name: error;\n    animation-duration: 1s;\n}\n\n@keyframes error {\n    0% {\n        background-color: red;\n    }\n    100% {\n        background-color: white;\n    }\n}\n\n.prettyplan ul {\n    padding-left: 0;\n    font-size: 13px;\n}\n\n.prettyplan li {\n    list-style: none;\n    background: #ffffffe6;\n    padding: 10px;\n    color: #000000c0;\n    margin: 4px;\n    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);\n}\n\n.prettyplan ul.warnings li {\n    border-left: 3px solid #757575;\n}\n\n.prettyplan ul.actions li.update {\n    border-left: 3px solid #ff8f00;\n}\n.prettyplan ul.actions li.create {\n    border-left: 3px solid #2e7d32;\n}\n.prettyplan ul.actions li.destroy {\n    border-left: 3px solid #b71c1c;\n}\n.prettyplan ul.actions li.recreate {\n    border-left: 3px solid #1565c0;\n}\n.prettyplan ul.actions li.read {\n    border-left: 3px solid #519bf0;\n}\n\n.badge {\n    display: inline-block;\n    text-transform: uppercase;\n    margin-right: 10px;\n    padding: 3px;\n    font-size: 12px;\n    font-weight: bold;\n}\n.warnings .badge {\n    color: #757575;\n}\nli.update .badge {\n    color: #ff8f00;\n}\nli.create .badge {\n    color: #2e7d32;\n}\nli.destroy .badge {\n    color: #b71c1c;\n}\nli.recreate .badge {\n    color: #1565c0;\n}\nli.read .badge {\n    color: #519bf0;\n}\n\n.id-segment:not(:last-child)::after {\n    content: \" > \";\n}\n.id-segment.name, .id-segment.type {\n    font-weight: bold;\n}\n\n.change-count {\n    float: right;\n}\n\n.summary {\n    cursor: pointer;\n}\n\n.changes {\n    margin: 5px auto 0 auto;\n    padding: 5px;\n}\n.changes table {\n    width: 100%;\n    word-break: break-all;\n    font-size: 13px;\n}\n.changes table td {\n    padding: 10px;\n    width: 40%;\n}\npre {\n    white-space: pre-wrap;\n}\n.changes table td.property {\n    width: 20%;\n    text-align: right;\n    font-weight: bold;\n}\n.changes table tr:nth-child(even) {\n    background-color: #f5f5f5;\n}\n\n.forces-new-resource {\n    color: #b71c1c;\n}\n\n.collapsed, .hidden {\n    display: none;\n}\n"
  },
  {
    "path": "src/ts/parse.ts",
    "content": "export interface ResourceId {\n    name: string;\n    type: string;\n    prefixes: string[];\n}\nexport interface Warning {\n    id: ResourceId;\n    detail: string;\n}\nexport enum ChangeType {\n    Create = 'create',\n    Read = 'read',\n    Update = 'update',\n    Destroy = 'destroy',\n    Recreate = 'recreate',\n    Unknown = 'unknown'\n}\nexport interface Diff {\n    property: string;\n    old?: string;\n    new: string;\n    forcesNewResource?: string;\n}\nexport interface Action {\n    id: ResourceId;\n    type: ChangeType;\n    changes: Diff[];\n}\nexport interface Plan {\n    warnings: Warning[];\n    actions: Action[];    \n}\n\nexport function parse(terraformPlan: string): Plan {\n    var warnings = parseWarnings(terraformPlan);\n\n    var changeSummary = extractChangeSummary(terraformPlan);\n    var changes = extractIndividualChanges(changeSummary);\n\n    var plan = { warnings: warnings, actions: [] };\n    for (var i = 0; i < changes.length; i++) {\n        plan.actions.push(parseChange(changes[i]));\n    }\n\n    return plan;\n}\n\nexport function parseWarnings(terraformPlan: string): Warning[] {\n    let warningRegex: RegExp = new RegExp('Warning: (.*:)(.*)', 'gm');\n    let warning: RegExpExecArray;\n    let warnings: Warning[] = [];\n\n    do {\n        warning = warningRegex.exec(terraformPlan);\n        if (warning) {\n            warnings.push({ id: parseId(warning[1]), detail: warning[2] });\n        }\n    } while (warning);\n\n    return warnings;\n}\n\nexport function extractChangeSummary(terraformPlan: string): string {\n    var beginActionRegex = new RegExp('Terraform will perform the following actions:', 'gm');\n    var begin = beginActionRegex.exec(terraformPlan);\n\n    if (begin) return terraformPlan.substring(begin.index + 45);\n    else return terraformPlan;\n}\n\nexport function extractIndividualChanges(changeSummary: string): string[] {\n    //TODO: Fix the '-/' in '-/+' getting chopped off\n    var changeRegex = new RegExp('([~+-]|-\\/\\+|<=) [\\\\S\\\\s]*?((?=-\\/\\+|[~+-] |<=|Plan:)|$)', 'g');\n    var change;\n    var changes = [];\n\n    do {\n        change = changeRegex.exec(changeSummary);\n        if (change) changes.push(change[0]);\n    } while (change);\n\n    return changes;\n}\n\nexport function parseChange(change: string): Action {\n    var changeTypeAndIdRegex = new RegExp('([~+-]|-\\/\\+|<=) (.*)$', 'gm');\n    var changeTypeAndId = changeTypeAndIdRegex.exec(change);\n    var changeTypeSymbol = changeTypeAndId[1];\n    var resourceId = changeTypeAndId[2];\n\n    var type;\n    type = parseChangeSymbol(changeTypeSymbol);\n\n    //Workaround for recreations showing up as '+' changes\n    if (resourceId.match('(new resource required)')) {\n        type = 'recreate';\n        resourceId = resourceId.replace(' (new resource required)', '');\n    }\n\n    var diffs;\n    if (type === 'create' || type === 'read') {\n        diffs = parseSingleValueDiffs(change);\n    }\n    else {\n        diffs = parseNewAndOldValueDiffs(change);\n    }\n\n    return {\n        id: parseId(resourceId),\n        type: type,\n        changes: diffs\n    };\n}\n\nexport function parseId(resourceId: string): ResourceId {\n    var idSegments = resourceId.split('.');\n    var resourceName = idSegments[idSegments.length - 1];\n    var resourceType = idSegments[idSegments.length - 2] || null;\n    var resourcePrefixes = idSegments.slice(0, idSegments.length - 2);\n\n    return { name: resourceName, type: resourceType, prefixes: resourcePrefixes };\n}\n\nexport function parseChangeSymbol(changeTypeSymbol): ChangeType {\n    if (changeTypeSymbol === \"-\")\n        return ChangeType.Destroy;\n    else if (changeTypeSymbol === \"+\")\n        return ChangeType.Create;\n    else if (changeTypeSymbol === \"~\")\n        return ChangeType.Update\n    else if (changeTypeSymbol === \"<=\")\n        return ChangeType.Read;\n    else if (changeTypeSymbol === \"-/+\")\n        return ChangeType.Recreate;\n    else\n        return ChangeType.Unknown;\n}\n\nexport function parseSingleValueDiffs(change): Diff[] {\n    var propertyAndValueRegex = new RegExp('\\\\s*(.*?): *(?:<computed>|\"(|[\\\\S\\\\s]*?[^\\\\\\\\])\")', 'gm');\n    var diff;\n    var diffs = [];\n\n    do {\n        diff = propertyAndValueRegex.exec(change);\n        if (diff) {\n            diffs.push({\n                property: diff[1].trim(),\n                new: diff[2] !== undefined ? diff[2] : \"<computed>\"\n            });\n        }\n    } while (diff);\n\n    return diffs;\n}\n\nexport function parseNewAndOldValueDiffs(change): Diff[] {\n    var propertyAndNewAndOldValueRegex = new RegExp('\\\\s*(.*?): *(?:\"(|[\\\\S\\\\s]*?[^\\\\\\\\])\")[\\\\S\\\\s]*?=> *(?:<computed>|\"(|[\\\\S\\\\s]*?[^\\\\\\\\])\")( \\\\(forces new resource\\\\))?', 'gm');\n    var diff;\n    var diffs = [];\n\n    do {\n        diff = propertyAndNewAndOldValueRegex.exec(change);\n        if (diff) {\n            diffs.push({\n                property: diff[1].trim(),\n                old: diff[2],\n                new: diff[3] !== undefined ? diff[3] : \"<computed>\",\n                forcesNewResource: diff[4] !== undefined\n            });\n        }\n    } while (diff);\n\n    return diffs;\n}"
  },
  {
    "path": "src/ts/prettyplan.ts",
    "content": "import { getCurrentVersion, getLastUsedVersion, updateLastUsedVersion } from './releases';\nimport { expandAll, collapseAll, accordion, closeModal } from './ui';\nimport { showReleaseNotification, hideReleaseNotification, showReleaseNotes, displayParsingErrorMessage, hideParsingErrorMessage, clearExistingOutput, unHidePlan, render } from './render';\nimport { parse } from './parse';\n\nwindow.addEventListener('load', function () {\n    if (getCurrentVersion() != getLastUsedVersion()) {\n        showReleaseNotification(getCurrentVersion());\n        updateLastUsedVersion();\n    }\n}); \n\n(<any>window).runPrettyplan = () => {\n    hideParsingErrorMessage();\n    clearExistingOutput();\n\n    var terraformPlan = (<HTMLTextAreaElement>document.getElementById(\"terraform-plan\")).value;\n    var plan = parse(terraformPlan);\n\n    if (plan.warnings.length === 0 && plan.actions.length === 0) {\n        displayParsingErrorMessage();\n    }\n\n    render(plan);\n    unHidePlan();\n}\n\n(<any>window).showReleaseNotes = showReleaseNotes;\n(<any>window).expandAll = expandAll;\n(<any>window).collapseAll = collapseAll;\n(<any>window).accordion = accordion;\n(<any>window).closeModal = closeModal;\n(<any>window).hideReleaseNotification = hideReleaseNotification;"
  },
  {
    "path": "src/ts/releases.ts",
    "content": "export interface Release {\n    version: string;\n    notes: string[];\n}\n\nexport function getCurrentVersion(): string {\n    return releases[0].version;\n}\n\nexport function getLastUsedVersion(): string {\n    return window.localStorage.getItem('lastUsedVersion');\n}\n\nexport function updateLastUsedVersion(): void {\n    window.localStorage.setItem('lastUsedVersion', getCurrentVersion());\n}\n\nexport function getReleases(): Release[] {\n    return releases;\n}\n\n//New releases should always go at the top of this list.\nlet releases: Release[] = [\n    {\n        version: 'v1.3',\n        notes: [\n            'A command-line version of Prettyplan is now available! Check it out on <a target=\"_blank\" href=\"https://github.com/chrislewisdev/prettyplan-cli\">GitHub</a>'\n        ]\n    },\n    {\n        version: 'v1.2',\n        notes: [\n            '<em>&lt;computed&gt;</em> values now display properly instead of being interpreted as HTML (<a target=\"_blank\" href=\"https://github.com/chrislewisdev/prettyplan/issues/2\">#2</a>)',\n            'Resource changes with <em>(forces new resource)</em> now have this highlighted in the table of changes (<a target=\"_blank\" href=\"https://github.com/chrislewisdev/prettyplan/issues/3\">#3</a>)',\n            'Italics for <em>&lt;computed&gt;</em> or <em>${variable}</em> values to help set them apart from regular values'\n        ]\n    },\n    {\n        version: 'v1.1',\n        notes: [\n            'Added handy release notes!',\n            'Fixed parsing of large AWS IAM policy documents (<a target=\"_blank\" href=\"https://github.com/chrislewisdev/prettyplan/issues/10\">#10</a>)'\n        ]\n    },\n    {\n        version: 'v1.0',\n        notes: [\n            'See your Terraform plans transformed into a beautiful tabulated format!',\n            'Support for prettyifying JSON content for easier reading',\n            'Theming consistent with the Terraform colour scheme',\n            'Works in Firefox, Chrome, and Edge'\n        ]\n    }\n];"
  },
  {
    "path": "src/ts/render.ts",
    "content": "import { Plan, Action, Diff, Warning, ResourceId } from './parse'\nimport { removeChildren, addClass, removeClass, createModalContainer } from './ui';\nimport { Release, getReleases } from './releases';\n\nexport function clearExistingOutput(): void {\n    removeChildren(document.getElementById('errors'));\n    removeChildren(document.getElementById('warnings'));\n    removeChildren(document.getElementById('actions'));\n}\n\nexport function hideParsingErrorMessage(): void {\n    addClass(document.getElementById('parsing-error-message'), 'hidden');\n}\n\nexport function displayParsingErrorMessage(): void {\n    removeClass(document.getElementById('parsing-error-message'), 'hidden');\n}\n\nexport function unHidePlan(): void {\n    removeClass(document.getElementById('prettyplan'), 'hidden');\n}\n\nexport function showReleaseNotification(version: string): void {\n    const notificationElement = document.getElementById('release-notification');\n    notificationElement.innerHTML = components.releaseNotification(version);\n    removeClass(notificationElement, 'hidden');\n}\n\nexport function hideReleaseNotification(): void {\n    addClass(document.getElementById('release-notification'), 'dismissed');\n}\n\nexport function showReleaseNotes(): void {\n    createModalContainer().innerHTML = components.modal(components.releaseNotes(getReleases()));\n}\n\nexport function render(plan: Plan): void {\n    if (plan.warnings) {\n        const warningList = document.getElementById('warnings');\n        warningList.innerHTML = plan.warnings.map(components.warning).join('');\n    }\n\n    if (plan.actions) {\n        const actionList = document.getElementById('actions');\n        actionList.innerHTML = plan.actions.map(components.action).join('');\n    }\n}\n\nconst components = {\n    badge: (label: string): string => `\n        <span class=\"badge\">${label}</span>\n    `,\n\n    id: (id: ResourceId): string => `\n        <span class=\"id\">\n            ${id.prefixes.map(prefix => \n                `<span class=\"id-segment prefix\">${prefix}</span>`\n            ).join('')}\n            <span class=\"id-segment type\">${id.type}</span>\n            <span class=\"id-segment name\">${id.name}</span>\n        </span>\n    `,\n\n    warning: (warning: Warning): string => `\n        <li>\n            ${components.badge('warning')}\n            ${components.id(warning.id)}\n            <span>${warning.detail}</span>\n        </li>\n    `,\n\n    changeCount: (count: number): string => `\n        <span class=\"change-count\">\n            ${count + ' change' + (count > 1 ? 's' : '')}\n        </span>\n    `,\n\n    change: (change: Diff): string => `\n        <tr>\n            <td class=\"property\">\n                ${change.property}\n                ${change.forcesNewResource ? `<br /><span class=\"forces-new-resource\">(forces new resource)</span>` : ''}\n            </td>\n            <td class=\"old-value\">${change.old ? prettify(change.old) : ''}</td>\n            <td class=\"new-value\">${prettify(change.new)}</td>\n        </tr>\n    `,\n\n    action: (action: Action): string => `\n        <li class=\"${action.type}\">\n            <div class=\"summary\" onclick=\"accordion(this)\">\n                ${components.badge(action.type)}\n                ${components.id(action.id)}\n                ${action.changes ? components.changeCount(action.changes.length) : ''}\n            </div>\n            <div class=\"changes collapsed\">\n                <table>\n                    ${action.changes.map(components.change).join('')}\n                </table>\n            </div>\n        </li>\n    `,\n\n    modal: (content: string): string => `\n        <div class=\"modal-pane\" onclick=\"closeModal()\"></div>\n        <div class=\"modal-content\">\n            <div class=\"modal-close\"><button class=\"text-button\" onclick=\"closeModal()\">close</button></div>\n            ${content}\n        </div>\n    `,\n\n    releaseNotes: (releases: Release[]): string => `\n        <div class=\"release-notes\">\n            <h1>Release Notes</h1>\n            ${releases.map(components.release).join('')}\n        </div>\n    `,\n\n    release: (release: Release): string => `\n        <h2>${release.version}</h2>\n        <ul>\n            ${release.notes.map((note) => `<li>${note}</li>`).join('')}\n        </ul>\n    `,\n\n    releaseNotification: (version: string): string => `\n        Welcome to ${version}!\n        <button class=\"text-button\" onclick=\"showReleaseNotes(); hideReleaseNotification()\">View release notes?</button>\n        (or <button class=\"text-button\" onclick=\"hideReleaseNotification()\">ignore</button>)\n    `\n};\n\nfunction prettify(value: string): string {\n    if (value === '<computed>')\n    {\n        return `<em>&lt;computed&gt;</em>`;\n    }\n    else if (value.startsWith('${') && value.endsWith('}'))\n    {\n        return `<em>${value}</em>`;\n    }\n    else if (value.indexOf('\\\\n') >= 0 || value.indexOf('\\\\\"') >= 0) {\n        var sanitisedValue = value.replace(new RegExp('\\\\\\\\n', 'g'), '\\n')\n                                  .replace(new RegExp('\\\\\\\\\"', 'g'), '\"');\n        \n        return `<pre>${prettifyJson(sanitisedValue)}</pre>`;\n    }\n    else {\n        return value;\n    }\n}\n\nfunction prettifyJson(maybeJson: string): string {\n    try {\n        return JSON.stringify(JSON.parse(maybeJson), null, 2);\n    }\n    catch (e) {\n        return maybeJson;\n    }\n}"
  },
  {
    "path": "src/ts/ui.ts",
    "content": "export function accordion(element: Element): void {\n    const changes = element.parentElement.getElementsByClassName('changes');\n    for (var i = 0; i < changes.length; i++) {\n        toggleClass(changes[i], 'collapsed');\n    }\n}\n\nexport function toggleClass(element: Element, className: string): void {\n    if (!element.className.match(className)) {\n        element.className += ' ' + className;\n    }\n    else {\n        element.className = element.className.replace(className, '');\n    }\n}\n\nexport function addClass(element: Element, className: string): void {\n    if (!element.className.match(className)) element.className += ' ' + className;\n}\n\nexport function removeClass(element: Element, className: string): void {\n    element.className = element.className.replace(className, '');\n}\n\nexport function expandAll(): void {\n    const sections = document.querySelectorAll('.changes.collapsed');\n\n    for (var i = 0; i < sections.length; i++) {\n        toggleClass(sections[i], 'collapsed');\n    }\n\n    toggleClass(document.querySelector('.expand-all'), 'hidden');\n    toggleClass(document.querySelector('.collapse-all'), 'hidden');\n}\n\nexport function collapseAll(): void {\n    const sections = document.querySelectorAll('.changes:not(.collapsed)');\n\n    for (var i = 0; i < sections.length; i++) {\n        toggleClass(sections[i], 'collapsed');\n    }\n\n    toggleClass(document.querySelector('.expand-all'), 'hidden');\n    toggleClass(document.querySelector('.collapse-all'), 'hidden');\n}\n\nexport function removeChildren(element: Element): void {\n    while (element.lastChild) {\n        element.removeChild(element.lastChild);\n    }\n}\n\nexport function createModalContainer(): HTMLElement {\n    const modalElement = document.createElement('div');\n    modalElement.id = 'modal-container';\n\n    document.body.appendChild(modalElement);\n\n    return modalElement;\n}\n\nexport function closeModal(): void {\n    const modalElement = document.getElementById('modal-container');\n    document.body.removeChild(modalElement);\n}"
  },
  {
    "path": "tests/extractChangeSummary.test.ts",
    "content": "import { extractChangeSummary } from '../src/ts/parse'\n\ntest('extract change summary - single line', function() {\n    const extractedSummary = extractChangeSummary('Terraform will perform the following actions:<summary>');\n\n    expect(extractedSummary).toBe('<summary>');\n});\n\ntest('extract change summary - multi line', function() {\n    const extractedSummary = extractChangeSummary(`\n        Text preceding the terraform plan\n\n        Terraform will perform the following actions:\n\n        <summary>`\n    );\n\n    expect(extractedSummary).toBe('\\n\\n        <summary>');\n});\n\ntest('extract change summary - without any Terraform summary prefix', function() {\n    const extractedSummary = extractChangeSummary('<summary>');\n\n    expect(extractedSummary).toBe('<summary>');\n});"
  },
  {
    "path": "tests/extractInvididualChanges.test.ts",
    "content": "import { extractIndividualChanges } from '../src/ts/parse';\n\ntest('extract individual changes - with plan summary at end', function() {\n    const changes = extractIndividualChanges(`\n      + module.alb.aws_alb_listener.default_https\n          ssl_policy:                                             \"old\" => \"new\"\n    \n      ~ module.api.aws_alb_listener_rule.default\n          condition.2636223071.field:                             \"path-pattern\" => \"\"\n          condition.2636223071.field:                             \"path-pattern\" => \"\"\n          condition.2636223071.field:                             \"path-pattern\" => \"\"\n    \n      - module.api.aws_alb_target_group.default\n          health_check.0.path:                                    \"/healthcheck/old\" => \"/healthcheck/new\"\n    \n      -/+ module.service_a.aws_ecs_service.default\n          task_definition:                                        \"service-a:185\" => \"service-a:179\"\n    \n      <= module.service_b.aws_ecs_service.default\n          task_definition:                                        \"service-b:171\" => \"service-b:165\"\n      Plan: 2 to add, 1 to change, 2 to destroy.\n    `);\n\n    expect(changes).toHaveLength(5);\n});\n\ntest('extract individual changes - without plan summary at end', function() {\n    const changes = extractIndividualChanges(`\n      + module.alb.aws_alb_listener.default_https\n          ssl_policy:                                             \"old\" => \"new\"\n    \n      ~ module.api.aws_alb_listener_rule.default\n          condition.2636223071.field:                             \"path-pattern\" => \"\"\n    `);\n\n    expect(changes).toHaveLength(2);\n});\n\ntest('extract individual changes - with extra text at the start', function() {\n    const changes = extractIndividualChanges(`\n      this text here should not be detected part of the change\n      neither should this\n      -------------------------------------------\n\n      + module.alb.aws_alb_listener.default_https\n          ssl_policy:                                             \"old\" => \"new\"\n    \n      ~ module.api.aws_alb_listener_rule.default\n          condition.2636223071.field:                             \"path-pattern\" => \"\"\n    `);\n\n    expect(changes).toHaveLength(2);\n});"
  },
  {
    "path": "tests/parseChangeSymbol.test.ts",
    "content": "import { parseChangeSymbol } from '../src/ts/parse';\n\ntest('parse change symbol', function() {\n    expect(parseChangeSymbol('+')).toBe('create');\n    expect(parseChangeSymbol('-')).toBe('destroy');\n    expect(parseChangeSymbol('~')).toBe('update');\n    expect(parseChangeSymbol('<=')).toBe('read');\n    expect(parseChangeSymbol('-/+')).toBe('recreate');\n    expect(parseChangeSymbol('gibberish')).toBe('unknown');\n});"
  },
  {
    "path": "tests/parseId.test.ts",
    "content": "import { parseId } from '../src/ts/parse'\n\ntest('parse id - no prefixes', function() {\n    const id = parseId('aws_route53_record.domain_name');\n\n    expect(id.name).toBe('domain_name');\n    expect(id.type).toBe('aws_route53_record');\n    expect(id.prefixes).toEqual([]);\n});\ntest('parse id - with prefixes', function() {\n    const id = parseId('module.api.aws_ecs_service.api_service');\n\n    expect(id.name).toBe('api_service');\n    expect(id.type).toBe('aws_ecs_service');\n    expect(id.prefixes).toEqual(['module', 'api']);\n});\ntest('parse id - name only', function() {\n    const id = parseId('api_service');\n\n    expect(id.name).toBe('api_service');\n    expect(id.type).toBeNull();\n    expect(id.prefixes).toEqual([]);\n});"
  },
  {
    "path": "tests/parseNewAndOldValueDiffs.test.ts",
    "content": "import { parseNewAndOldValueDiffs } from '../src/ts/parse';\n\ntest('new and old value diffs - quote formatting', function() {\n    const diffs = parseNewAndOldValueDiffs('property_name: \"old_value\" => \"new_value\"');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('property_name');\n    expect(diffs[0].old).toBe('old_value');\n    expect(diffs[0].new).toBe('new_value');\n});\ntest('new and old value diffs - empty quotes', function() {\n    const diffs = parseNewAndOldValueDiffs('property_name: \"\" => \"new_value\"');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('property_name');\n    expect(diffs[0].old).toBe('');\n    expect(diffs[0].new).toBe('new_value');\n});\ntest('new and old value diffs - computed values', function() {\n    const diffs = parseNewAndOldValueDiffs('property_name: \"old_value\" => <computed>');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('property_name');\n    expect(diffs[0].old).toBe('old_value');\n    expect(diffs[0].new).toBe('<computed>');\n});\ntest('new and old value diffs - whitespace handling', function() {\n    const diffs = parseNewAndOldValueDiffs('   property_name   : \" old_value \" => \"new_value \"');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('property_name');\n    expect(diffs[0].old).toBe(' old_value ');\n    expect(diffs[0].new).toBe('new_value ');\n});\ntest('new and old value diffs - multi line', function() {\n    const diffs = parseNewAndOldValueDiffs('property1: \"old1\" => \"new1\"\\n property2: \"old2\" => \"new2\"');\n\n    expect(diffs).toHaveLength(2);\n    expect(diffs[0].property).toBe('property1');\n    expect(diffs[0].old).toBe('old1');\n    expect(diffs[0].new).toBe('new1');\n    expect(diffs[1].property).toBe('property2');\n    expect(diffs[1].old).toBe('old2');\n    expect(diffs[1].new).toBe('new2');\n});\ntest('new and old value diffs - IAM policy document', function() {\n    const diffs = parseNewAndOldValueDiffs(`\n    policy:                                     \"{\\\\r\\\\n    \\\\\"Version\\\\\": \\\\\"2012-10-17\\\\\",\\\\r\\\\n    \\\\\"Statement\\\\\": [\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"sqs:*\\\\\",\\\\r\\\\n                \\\\\"sns:*\\\\\"\\\\r\\\\n\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": \\\\\"*\\\\\"\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": [\\\\r\\\\n                \\\\\"*\\\\\",\\\\r\\\\n                \\\\\"*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"logs:CreateLogGroup\\\\\",\\\\r\\\\n                \\\\\"logs:CreateLogStream\\\\\",\\\\r\\\\n                \\\\\"logs:PutLogEvents\\\\\"\\\\r\\\\n            ]\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": [\\\\r\\\\n                \\\\\"*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n\n        \\\\\"s3:PutObject\\\\\",\\\\r\\\\n                \\\\\"s3:GetObject\\\\\",\\\\r\\\\n                \\\\\"s3:GetObjectVersion\\\\\"\\\\r\\\\n            ]\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"cloudformation:*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Resource\\\\\": \\\\\"*\\\\\"\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n\"\n    => \"{\\\\r\\\\n    \\\\\"Version\\\\\": \\\\\"2012-10-17\\\\\",\\\\r\\\\n    \\\\\"Statement\\\\\": [\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"sqs:*\\\\\",\\\\r\\\\n                \\\\\"sns:*\\\\\"\\\\r\\\\n\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": \\\\\"*\\\\\"\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": [\\\\r\\\\n                \\\\\"*\\\\\",\\\\r\\\\n                \\\\\"*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"logs:CreateLogGroup\\\\\",\\\\r\\\\n                \\\\\"logs:CreateLogStream\\\\\",\\\\r\\\\n                \\\\\"logs:PutLogEvents\\\\\"\\\\r\\\\n            ]\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": [\\\\r\\\\n                \\\\\"*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n\n        \\\\\"s3:PutObject\\\\\",\\\\r\\\\n                \\\\\"s3:GetObject\\\\\",\\\\r\\\\n                \\\\\"s3:GetObjectVersion\\\\\"\\\\r\\\\n            ]\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"cloudformation:*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Resource\\\\\": \\\\\"*\\\\\"\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n\"\n    `);\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('policy');\n});\ntest('force new resource - false for normal diffs', function() {\n    const diffs = parseNewAndOldValueDiffs('property_name: \"old_value\" => \"new_value\"');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].forcesNewResource).toBe(false);\n});\ntest('force new resource - true when included', function() {\n    const diffs = parseNewAndOldValueDiffs('property_name: \"old_value\" => \"new_value\" (forces new resource)');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].forcesNewResource).toBe(true);\n});\ntest('force new resource - works for <computed> values', function() {\n    const diffs = parseNewAndOldValueDiffs('property_name: \"old_value\" => <computed> (forces new resource)');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].forcesNewResource).toBe(true);\n});"
  },
  {
    "path": "tests/parseSingleValueDiffs.test.ts",
    "content": "import { parseSingleValueDiffs } from '../src/ts/parse';\n\ntest('single value diffs - quote formatting', function() {\n    const diffs = parseSingleValueDiffs('property_name: \"new_value\"');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('property_name');\n    expect(diffs[0].new).toBe('new_value');\n});\ntest('single value diffs - empty quotes', function() {\n    const diffs = parseSingleValueDiffs('property_name: \"\"');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('property_name');\n    expect(diffs[0].new).toBe('');\n});\ntest('single value diffs - computed values', function() {\n    const diffs = parseSingleValueDiffs('property_name: <computed>');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('property_name');\n    expect(diffs[0].new).toBe('<computed>');\n});\ntest('single value diffs - whitespace handling', function() {\n    const diffs = parseSingleValueDiffs('     property_name :    \" value \"   ');\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('property_name');\n    expect(diffs[0].new).toBe(' value ');\n});\ntest('single value diffs - multi line', function() {\n    const diffs = parseSingleValueDiffs('property1: \"value1\"\\n property2: \"value2\"');\n\n    expect(diffs).toHaveLength(2);\n    expect(diffs[0].property).toBe('property1');\n    expect(diffs[0].new).toBe('value1');\n    expect(diffs[1].property).toBe('property2');\n    expect(diffs[1].new).toBe('value2');\n});\ntest('single value diffs - IAM policy document', function() {\n    const diffs = parseSingleValueDiffs(`\n    policy:                                     \"{\\\\r\\\\n    \\\\\"Version\\\\\": \\\\\"2012-10-17\\\\\",\\\\r\\\\n    \\\\\"Statement\\\\\": [\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"sqs:*\\\\\",\\\\r\\\\n                \\\\\"sns:*\\\\\"\\\\r\\\\n\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": \\\\\"*\\\\\"\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": [\\\\r\\\\n                \\\\\"*\\\\\",\\\\r\\\\n                \\\\\"*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"logs:CreateLogGroup\\\\\",\\\\r\\\\n                \\\\\"logs:CreateLogStream\\\\\",\\\\r\\\\n                \\\\\"logs:PutLogEvents\\\\\"\\\\r\\\\n            ]\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Resource\\\\\": [\\\\r\\\\n                \\\\\"*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n\n        \\\\\"s3:PutObject\\\\\",\\\\r\\\\n                \\\\\"s3:GetObject\\\\\",\\\\r\\\\n                \\\\\"s3:GetObjectVersion\\\\\"\\\\r\\\\n            ]\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n            \\\\\"Action\\\\\": [\\\\r\\\\n                \\\\\"cloudformation:*\\\\\"\\\\r\\\\n            ],\\\\r\\\\n            \\\\\"Resource\\\\\": \\\\\"*\\\\\"\\\\r\\\\n        },\\\\r\\\\n        {\\\\r\\\\n            \\\\\"Effect\\\\\": \\\\\"Allow\\\\\",\\\\r\\\\n\"\n    `);\n\n    expect(diffs).toHaveLength(1);\n    expect(diffs[0].property).toBe('policy');\n});"
  },
  {
    "path": "tests/parseWarnings.test.ts",
    "content": "import { parseWarnings } from '../src/ts/parse';\n\ntest('parse warnings - single warning', function() {\n    const warnings = parseWarnings('Warning: resource_name: <warning detail>');\n\n    expect(warnings).toHaveLength(1);\n    expect(warnings[0].id.name).toBe('resource_name:');\n    expect(warnings[0].detail).toBe(' <warning detail>');\n});\n\ntest('parse warnings - multiple warning', function() {\n    const warnings = parseWarnings('Warning: r1: w1\\nWarning: r2: w2\\nWarning: r3: w3');\n\n    expect(warnings).toHaveLength(3);\n\n    expect(warnings[0].id.name).toBe('r1:');\n    expect(warnings[0].detail).toBe(' w1');\n\n    expect(warnings[1].id.name).toBe('r2:');\n    expect(warnings[1].detail).toBe(' w2');\n\n    expect(warnings[2].id.name).toBe('r3:');\n    expect(warnings[2].detail).toBe(' w3');\n});\n\ntest('parse warnings - no warnings', function() {\n    const warnings = parseWarnings('Here are some things that are NOT warnings');\n\n    expect(warnings).toHaveLength(0);\n});"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"outDir\": \"./dist/\",\n        \"module\": \"es6\",\n        \"target\": \"es6\"\n    }\n}"
  },
  {
    "path": "webpack.config.js",
    "content": "const path = require('path');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst CopyWebpackPlugin = require('copy-webpack-plugin');\n\nmodule.exports = {\n    mode: 'development',\n    entry: './src/ts/prettyplan.ts',\n    devServer: {\n        contentBase: './dist'\n    },\n    plugins: [\n        new HtmlWebpackPlugin({ template: 'src/index.html' }),\n        new CopyWebpackPlugin(['src/style.css'])\n    ],\n    output: {\n        filename: 'app.js',\n        path: path.resolve(__dirname, 'dist')\n    },\n    resolve: { extensions: ['.ts', '.js'] },\n    module: {\n        rules: [\n            {\n                test: /\\.ts$/,\n                use: 'ts-loader',\n                exclude: /node_modules/\n            }\n        ]\n    }\n};"
  }
]