├── .gitignore ├── src ├── index.ts └── hcl.ts ├── .github └── workflows │ ├── test.yaml │ ├── publish.yaml │ └── static.yaml ├── tsconfig.json ├── package.json ├── LICENSE.md ├── README.md ├── tests └── highlight.test.ts └── public └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import hcl from './hcl.js' 2 | 3 | export default hcl; -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Run Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | workflow_dispatch: 8 | 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Use Node.js 23 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 23 22 | 23 | # https://github.com/vitejs/vite/discussions/15532 24 | - name: Install dependencies 25 | run: npm install && npm install @rollup/rollup-linux-x64-gnu --save-optional 26 | 27 | - name: Run tests 28 | run: npm test 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish package to npm 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 23 17 | registry-url: 'https://registry.npmjs.org' 18 | 19 | - name: Install dependencies 20 | run: npm install && npm install @rollup/rollup-linux-x64-gnu --save-optional 21 | 22 | - name: Run tests 23 | run: npm test 24 | 25 | - name: Publish to npm 26 | run: npm run build && npm publish --provenance --access public 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* https://www.totaltypescript.com/tsconfig-cheat-sheet */ 2 | { 3 | "compilerOptions": { 4 | /* Base Options: */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es2022", 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | /* Strictness */ 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | "noImplicitOverride": true, 17 | /* If transpiling with TypeScript: */ 18 | "module": "NodeNext", 19 | "outDir": "dist", 20 | "sourceMap": true, 21 | /* if you're building for a library: */ 22 | "declaration": true, 23 | /* If your code doesn't run in the DOM: */ 24 | "lib": [ 25 | "es2022", 26 | "dom", 27 | "dom.iterable" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "highlight-hcl", 3 | "version": "1.0.0", 4 | "description": "A highlight.js plugin for hashicorp configuration language", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "/dist" 10 | ], 11 | "scripts": { 12 | "test": "vitest", 13 | "build": "tsc", 14 | "dev": "tsc --watch" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/yilanboy/highlight-hcl.git" 19 | }, 20 | "keywords": [ 21 | "highlight.js", 22 | "highlightjs", 23 | "syntax", 24 | "highlight", 25 | "terraform", 26 | "packer", 27 | "hcl" 28 | ], 29 | "author": "yilanboy", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/yilanboy/highlight-hcl/issues" 33 | }, 34 | "homepage": "https://github.com/yilanboy/highlight-hcl#readme", 35 | "devDependencies": { 36 | "typescript": "^5.7.2", 37 | "vitest": "^2.1.8" 38 | }, 39 | "dependencies": { 40 | "highlight.js": "^11.11.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright © Allen Jiang 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Highlight HCL 2 | 3 | Highlight HashiCorp configuration language (HCL) with [highlight.js](https://highlightjs.org/). 4 | You can use this library to highlight Terraform, OpenTofu, and Packer. 5 | 6 | You can see the highlight results [here](https://yilanboy.github.io/highlight-hcl/public/). 7 | 8 | ## Installation 9 | 10 | Using npm to download the library. 11 | 12 | ```bash 13 | npm install highlight.js highlight-hcl 14 | ``` 15 | 16 | ## Importing the Library 17 | 18 | To use the HCL definition with highlight.js, you have two options for importing: 19 | 20 | ### Optimized Import (Recommended) 21 | 22 | Load only the language definitions you need. 23 | 24 | ```javascript 25 | // import core hljs api and required languages 26 | import hljs from 'highlight.js/lib/core'; 27 | import hcl from 'highlight-hcl'; 28 | 29 | // register language definition 30 | hljs.registerLanguage('hcl', hcl); 31 | ``` 32 | 33 | ### Full Import 34 | 35 | Load all languages of highlight.js, please note that this generates a large file. 36 | 37 | ```javascript 38 | import hljs from 'highlight.js'; 39 | import hcl from 'highlight-hcl'; 40 | 41 | hljs.registerLanguage('hcl', hcl); 42 | ``` 43 | 44 | More information about importing highlight.js library, please refer 45 | to [here](https://highlightjs.readthedocs.io/en/latest/readme.html#importing-the-library). 46 | -------------------------------------------------------------------------------- /.github/workflows/static.yaml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: [ "main" ] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: '.' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /tests/highlight.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from 'vitest'; 2 | import hljs from 'highlight.js/lib/core'; 3 | import hcl from '../src/index.js'; 4 | 5 | hljs.registerLanguage('hcl', hcl); 6 | 7 | describe('highlight hashicorp configuration language', () => { 8 | it('should highlight the "resource" as keyword', () => { 9 | const code = 'resource "aws_vpc" "main" {'; 10 | const result = hljs.highlightAuto(code, ['hcl']); 11 | 12 | expect(result.value) 13 | .to.contain('resource'); 14 | }); 15 | 16 | it('should highlight the "data" as keyword', () => { 17 | const code = 'data "aws_vpc" "main" {'; 18 | const result = hljs.highlightAuto(code, ['hcl']); 19 | 20 | expect(result.value) 21 | .to.contain('data'); 22 | }); 23 | 24 | it('should highlight the "module" as keyword', () => { 25 | const code = 'module "main" {'; 26 | const result = hljs.highlightAuto(code, ['hcl']); 27 | 28 | expect(result.value) 29 | .to.contain('module'); 30 | }); 31 | 32 | it('should highlight the "variable" as keyword', () => { 33 | const code = 'variable "subnet_arn" {'; 34 | const result = hljs.highlightAuto(code, ['hcl']); 35 | 36 | expect(result.value) 37 | .to.contain('variable'); 38 | }); 39 | 40 | it('should highlight the "assert" as keyword', () => { 41 | const code = 'assert {'; 42 | const result = hljs.highlightAuto(code, ['hcl']); 43 | 44 | expect(result.value) 45 | .to.contain('assert'); 46 | }); 47 | 48 | it('should highlight the if expression', () => { 49 | const code = 'condition ? true_val : false_val'; 50 | const result = hljs.highlightAuto(code, ['hcl']); 51 | 52 | expect(result.value) 53 | .to.contain('?') 54 | .to.contain(':'); 55 | }); 56 | 57 | it('should highlight the for expression', () => { 58 | const code = '{for s in var.list : s => upper(s)}'; 59 | const result = hljs.highlightAuto(code, ['hcl']); 60 | 61 | expect(result.value) 62 | .to.contain('for') 63 | .to.contain('in') 64 | .to.contain(':') 65 | .to.contain('=>'); 66 | }); 67 | 68 | it('should highlight "max()" as function', () => { 69 | const code = 'max(5, 12, 9)'; 70 | const result = hljs.highlightAuto(code, ['hcl']); 71 | 72 | expect(result.value) 73 | .to.contain('max'); 74 | }); 75 | 76 | it('should highlight "cidr_block" as attribute', () => { 77 | const code = 'cidr_block = each.value.cidr_block'; 78 | const result = hljs.highlightAuto(code, ['hcl']); 79 | 80 | expect(result.value) 81 | .to.contain('cidr_block'); 82 | }); 83 | 84 | it('should highlight heredoc', () => { 85 | const code = `block { 86 | value = <<<EOT\n' + 95 | 'hello\n' + 96 | 'world\n' + 97 | 'EOT'); 98 | }); 99 | 100 | 101 | it('should highlight indented heredoc', () => { 102 | const code = `block { 103 | value = <<-EOT 104 | hello 105 | world 106 | EOT 107 | }`; 108 | const result = hljs.highlightAuto(code, ['hcl']); 109 | 110 | expect(result.value) 111 | .to.contain('<<-EOT\n' + 112 | ' hello\n' + 113 | ' world\n' + 114 | 'EOT'); 115 | }); 116 | }); -------------------------------------------------------------------------------- /src/hcl.ts: -------------------------------------------------------------------------------- 1 | import type {CallbackResponse, HLJSApi} from 'highlight.js'; 2 | 3 | export default function (hljs: HLJSApi) { 4 | // map 'resource' in 'resource "aws_vpc" "main" {' 5 | // map 'data' in 'data "aws_vpc" "main" {' 6 | // map 'module' in 'module "main" {' 7 | // map 'ingress' in 'ingress {' 8 | // and more... 9 | const KEYWORDS = { 10 | scope: 'keyword', 11 | match: /\b(?\w+)\b(?=(?:\s+".+")*\s*\{)/, 12 | }; 13 | 14 | // for, in and if 15 | const FOR_KEYWORD = { 16 | scope: 'keyword', 17 | match: /(?<=\b)(?for|in|if)(?=\b)/, 18 | }; 19 | 20 | // the '?' in Conditional Expressions 21 | // condition ? true_val : false_val 22 | const QUESTION_MARK_IN_EXPRESSION = { 23 | scope: 'keyword', 24 | match: /(?\?)/, 25 | }; 26 | 27 | // the ':' in Conditional Expressions 28 | // condition ? true_val : false_val 29 | const COLON_IN_EXPRESSION = { 30 | scope: 'keyword', 31 | match: /(?:)/, 32 | }; 33 | 34 | // => 35 | const ARROW_EXPRESSION = { 36 | scope: 'keyword', 37 | match: /(?=>)/, 38 | }; 39 | 40 | // true 41 | // false 42 | // null 43 | const LITERAL = { 44 | scope: 'literal', 45 | match: /(?\btrue|false|null\b)/ 46 | }; 47 | 48 | // string 49 | // number 50 | // bool 51 | const TYPE = { 52 | scope: 'type', 53 | match: /(?\bstring|number|bool\b)/ 54 | }; 55 | 56 | // 1 or 1.2 57 | const NUMBERS = { 58 | scope: 'number', 59 | match: /\b(?\d+(\.\d+)?)\b/, 60 | }; 61 | 62 | // "string" 63 | // "string and ${variable}" 64 | const STRINGS = { 65 | scope: 'string', 66 | begin: /(?")/, 67 | end: /(?")/, 68 | contains: [ 69 | { 70 | scope: 'subst', 71 | begin: /(?\$\{)/, 72 | end: /(?})/, 73 | }, 74 | ], 75 | }; 76 | 77 | // <\w+)\n/, 89 | end: /[ \t]*(?\w+)\b/, 90 | 'on:begin': (match: string[], response: CallbackResponse) => { 91 | response.data._beginMatch = match[1] || match[2]; 92 | }, 93 | 'on:end': (match: string[], response: CallbackResponse) => { 94 | if (response.data._beginMatch !== match[1]) { 95 | response.ignoreMatch(); 96 | } 97 | }, 98 | }; 99 | 100 | // somethingLikeThis( 101 | const FUNCTION = { 102 | scope: 'title.function', 103 | match: /(?[a-zA-Z0-9_]+)(?=\()/, 104 | }; 105 | 106 | // somethingLikeThis = 107 | // exclude the case like 'bucket =' in 'aws_s3_bucket.main.bucket == "something"' 108 | const RESOURCE_ATTRIBUTE = { 109 | scope: 'attr', 110 | match: /(?[\w\-]+)(?=\s*=[^=>])/, 111 | }; 112 | 113 | // {} 114 | // [] 115 | // () 116 | // , 117 | const PUNCTUATIONS = { 118 | scope: 'punctuation', 119 | match: /(?[{}\[\](),])/, 120 | }; 121 | 122 | // > and < 123 | // + and - 124 | // * and / 125 | // == and != 126 | // <= and >= 127 | // ! 128 | const OPERATORS = { 129 | scope: 'operator', 130 | match: /(?[><+\-*\/]|<=|>=|==|!=|!)/, 131 | }; 132 | 133 | // aws_instance.main.public_ip 134 | // the 'aws_instance.main' and '.public_ip' in 'aws_instance.main[0].public_ip' 135 | // the 'aws_instance.main' and '.public_ip' in 'aws_instance.main["web"].public_ip' 136 | const ATTRIBUTE = { 137 | scope: 'attr', 138 | match: /(?[a-zA-Z0-9._*]+)/, 139 | }; 140 | 141 | return { 142 | case_insensitive: false, 143 | aliases: ['tf', 'hcl', 'terraform', 'opentofu', 'packer'], 144 | contains: [ 145 | hljs.COMMENT(/#/, /$/), 146 | KEYWORDS, 147 | FOR_KEYWORD, 148 | QUESTION_MARK_IN_EXPRESSION, 149 | COLON_IN_EXPRESSION, 150 | ARROW_EXPRESSION, 151 | LITERAL, 152 | TYPE, 153 | NUMBERS, 154 | STRINGS, 155 | HEREDOC, 156 | FUNCTION, 157 | RESOURCE_ATTRIBUTE, 158 | PUNCTUATIONS, 159 | OPERATORS, 160 | ATTRIBUTE 161 | ], 162 | }; 163 | } 164 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Highlight HCL 7 | 9 | 10 | 11 | 13 | 14 | 39 | 40 | 41 |
42 |

Examples of Highlight HCL

43 | 44 |

Highlight HashiCorp configuration language (HCL) with highlight.js.

45 | 46 |

Terraform

47 | 48 |

# Basic

49 | 50 |
 51 |         terraform {
 52 |   required_providers {
 53 |     aws = {
 54 |       source  = "hashicorp/aws"
 55 |       version = "~> 4.16"
 56 |     }
 57 |   }
 58 | 
 59 |   required_version = ">= 1.2.0"
 60 | }
 61 | 
 62 | provider "aws" {
 63 |   region  = "us-west-2"
 64 | }
 65 | 
 66 | resource "aws_instance" "app_server" {
 67 |   ami           = "ami-830c94e3"
 68 |   instance_type = "t2.micro"
 69 | 
 70 |   tags = {
 71 |     Name = "${var.instance_name} - PoC"
 72 |   }
 73 | }
 74 |     
75 | 76 |

# Input Variables

77 | 78 |
 79 |         variable "image_id" {
 80 |   type = string
 81 | }
 82 | 
 83 | variable "availability_zone_names" {
 84 |   type    = list(string)
 85 |   default = ["us-west-1a"]
 86 | }
 87 | 
 88 | variable "docker_ports" {
 89 |   type = list(object({
 90 |     internal = number
 91 |     external = number
 92 |     protocol = string
 93 |   }))
 94 | 
 95 |   default = [
 96 |     {
 97 |       internal = 8300
 98 |       external = 8300
 99 |       protocol = "tcp"
100 |     }
101 |   ]
102 | }
103 |     
104 | 105 |

# Output Values

106 | 107 |
108 |         output "instance_ip_addr" {
109 |   value = aws_instance.server["host_1"].private_ip
110 | }
111 | 
112 | output "instance_ip_addr" {
113 |   value = aws_instance.server[0].private_ip
114 | }
115 |     
116 | 117 |

# Local Values

118 | 119 |
120 |         locals {
121 |   # Ids for multiple sets of EC2 instances, merged together
122 |   instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
123 | }
124 | 
125 | locals {
126 |   # Common tags to be assigned to all resources
127 |   common_tags = {
128 |     Service = local.service_name
129 |     Owner   = local.owner
130 |   }
131 | }
132 |     
133 | 134 |

# For Loop

135 | 136 |
137 |         variable "users" {
138 |   type = map(object({
139 |     is_admin = bool
140 |   }))
141 | }
142 | 
143 | locals {
144 |   admin_users = {
145 |     for name, user in var.users : name => user
146 |     if user.is_admin
147 |   }
148 |   regular_users = {
149 |     for name, user in var.users : name => user
150 |     if !user.is_admin
151 |   }
152 | }
153 |     
154 | 155 |

# Module

156 | 157 |
158 |         module "servers" {
159 |   source = "./app-cluster"
160 | 
161 |   servers = 5
162 | }
163 |     
164 | 165 |

# Meta-Arguments

166 | 167 |
168 |         resource "aws_instance" "example" {
169 |   ami           = "ami-a1b2c3d4"
170 |   instance_type = "t2.micro"
171 | 
172 |   iam_instance_profile = aws_iam_instance_profile.example
173 | 
174 |   # The depends_on Meta-Argument
175 |   depends_on = [
176 |     aws_iam_role_policy.example,
177 |     aws_iam_role_policy.example_two
178 |   ]
179 | }
180 | 
181 | resource "azurerm_resource_group" "rg" {
182 |   # The for_each Meta-Argument
183 |   for_each = tomap({
184 |     a_group       = "eastus"
185 |     another_group = "westus2"
186 |   })
187 | 
188 |   name     = each.key
189 |   location = each.value
190 | }
191 |     
192 | 193 |

# Heredocs

194 | 195 |
196 |         block {
197 |   value = <<EOT
198 | hello
199 | world
200 | EOT
201 | }
202 | 
203 | block {
204 |   value = <<-EOT
205 |   hello
206 |     world
207 |   EOT
208 | }
209 |     
210 | 211 |

Packer

212 | 213 |

# Basic

214 | 215 |
216 |         packer {
217 |   required_plugins {
218 |     amazon = {
219 |       version = ">= 1.2.8"
220 |       source  = "github.com/hashicorp/amazon"
221 |     }
222 |   }
223 | }
224 | 
225 | source "amazon-ebs" "ubuntu" {
226 |   ami_name      = "learn-packer-linux-aws"
227 |   instance_type = "t2.micro"
228 |   region        = "us-west-2"
229 |   source_ami_filter {
230 |     filters = {
231 |       name                = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
232 |       root-device-type    = "ebs"
233 |       virtualization-type = "hvm"
234 |     }
235 |     most_recent = true
236 |     owners      = ["099720109477"]
237 |   }
238 |   ssh_username = "ubuntu"
239 | }
240 | 
241 | build {
242 |   name    = "learn-packer"
243 |   sources = [
244 |     "source.amazon-ebs.ubuntu"
245 |   ]
246 | }
247 |     
248 |
249 | 250 | 257 | 258 | --------------------------------------------------------------------------------