├── .gitignore ├── src ├── index.ts └── blade.ts ├── .github └── workflows │ ├── test.yaml │ ├── publish.yaml │ └── static.yaml ├── tsconfig.json ├── LICENSE.md ├── package.json ├── 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 blade from './blade.js' 2 | 3 | export default blade; -------------------------------------------------------------------------------- /.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 runs in the DOM: */ 24 | "lib": [ 25 | "es2022", 26 | "dom", 27 | "dom.iterable" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "highlight-blade", 3 | "version": "1.1.0", 4 | "description": "A highlight.js plugin for laravel blade template", 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-blade.git" 19 | }, 20 | "keywords": [ 21 | "highlight.js", 22 | "highlightjs", 23 | "syntax", 24 | "highlight", 25 | "laravel", 26 | "blade", 27 | "alpine.js", 28 | "alpinejs" 29 | ], 30 | "author": "yilanboy", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/yilanboy/highlight-blade/issues" 34 | }, 35 | "homepage": "https://github.com/yilanboy/highlight-blade#readme", 36 | "devDependencies": { 37 | "typescript": "^5.7.2", 38 | "vitest": "^2.1.8" 39 | }, 40 | "dependencies": { 41 | "highlight.js": "^11.11.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Highlight Blade 2 | 3 | Highlight Laravel Blade & Alpine.js syntax with [highlight.js](https://highlightjs.org/). 4 | 5 | You can see the highlight results [here](https://yilanboy.github.io/highlight-blade/public/). 6 | 7 | ## Installation 8 | 9 | Using npm to download the library. 10 | 11 | ```bash 12 | npm install highlight.js hightlight-blade 13 | ``` 14 | 15 | ## Importing the Library 16 | 17 | To use the Blade template definition with highlight.js, you have two options for importing: 18 | 19 | ### Optimized Import (Recommended) 20 | 21 | Load only the language definitions you need. 22 | 23 | ```javascript 24 | // import core hljs api and required languages 25 | import hljs from 'highlight.js/lib/core'; 26 | import javascript from 'highlight.js/lib/languages/javascript'; 27 | import xml from 'highlight.js/lib/languages/xml'; 28 | import php from 'highlight.js/lib/languages/php'; 29 | import blade from 'highlight-blade'; 30 | 31 | // register each language definition 32 | hljs.registerLanguage('javascript', javascript); 33 | hljs.registerLanguage('xml', xml); 34 | hljs.registerLanguage('php', php); 35 | hljs.registerLanguage('blade', blade); 36 | ``` 37 | 38 | ### Full Import 39 | 40 | Load all languages of highlight.js, please note that this generates a large file. 41 | 42 | ```javascript 43 | import hljs from 'highlight.js'; 44 | import blade from 'highlight-blade'; 45 | 46 | hljs.registerLanguage('blade', blade); 47 | ``` 48 | 49 | More information about importing highlight.js library, please refer 50 | to [here](https://highlightjs.readthedocs.io/en/latest/readme.html#importing-the-library). 51 | 52 | ## TODO 53 | 54 | - [x] Support Laravel Blade syntax 55 | - [x] Support Alpine.js syntax 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/blade.ts: -------------------------------------------------------------------------------- 1 | import type {CallbackResponse, HLJSApi, Language} from 'highlight.js'; 2 | 3 | function countChar(string: string, char: string) { 4 | let count = 0; 5 | for (let i = 0; i < string.length; i++) { 6 | if (string[i] === char) { 7 | count++; 8 | } 9 | } 10 | 11 | return count; 12 | } 13 | 14 | export default function (hljs: HLJSApi): Language { 15 | // {{ $escaped ? 'true' : 'false' }} 16 | const ESCAPED_TEMPLATE_VARIABLE = { 17 | begin: /(?\{\{)/, 18 | beginScope: 'template-variable', 19 | end: /(?}})/, 20 | endScope: 'template-variable', 21 | subLanguage: 'php', 22 | }; 23 | 24 | // {{ $unescaped ? '

true

' : '

false

' }} 25 | const UNESCAPED_TEMPLATE_VARIABLE = { 26 | begin: /(?\{!!)/, 27 | beginScope: 'template-variable', 28 | end: /(?!!})/, 29 | endScope: 'template-variable', 30 | subLanguage: 'php', 31 | }; 32 | 33 | // @php 34 | // $foo = 'bar'; 35 | // @endphp 36 | const RAW_PHP = { 37 | begin: /(?@php)/, 38 | beginScope: 'keyword', 39 | end: /(?@endphp)/, 40 | endScope: 'keyword', 41 | subLanguage: 'php', 42 | }; 43 | 44 | // :class="open ? '' : 'hidden'" 45 | // 46 | const ALPINE_JS_STATEMENT_AFTER_X_BIND_SHORTHAND_SYNTAX = { 47 | begin: /(?<=\s)(?::\w+=")/, 48 | excludeBegin: true, 49 | end: /(?")/, 50 | excludeEnd: true, 51 | subLanguage: 'javascript', 52 | } 53 | 54 | // :blade-value="$phpVar" 55 | const BLADE_COMPONENT_ATTRIBUTE = { 56 | begin: /(?<=\s)(?:[\w-]+=")/, 57 | excludeBegin: true, 58 | end: /(?")/, 59 | excludeEnd: true, 60 | subLanguage: 'php', 61 | }; 62 | 63 | // @click 64 | // @mousemove.shift 65 | const ALPINE_JS_X_ON_SHORTHAND_SYNTAX = { 66 | match: /(?@[a-z0-9:.-]+)(?==")/, 67 | scope: 'title.function', 68 | } 69 | 70 | // @click="toggle()" 71 | const ALPINE_JS_STATEMENT_AFTER_X_ON_SHORTHAND_SYNTAX = { 72 | begin: /(?<=\s@[a-z0-9:.-]+=)(?")/, 73 | excludeBegin: true, 74 | end: /(?")/, 75 | excludeEnd: true, 76 | subLanguage: 'javascript', 77 | } 78 | 79 | // @somethingLikeThis 80 | const BLADE_DIRECTIVES = { 81 | scope: 'keyword', 82 | match: /(?@[a-zA-Z]+)/, 83 | }; 84 | 85 | 86 | // @foreach ($list as $item) 87 | // or 88 | // @foreach($list as $item) 89 | const STATEMENT_AFTER_BLADE_DIRECTIVES = { 90 | begin: /(?<=@[a-zA-Z]+\s?)(?\()/, 91 | beginScope: 'punctuation', 92 | end: /(?\))/, 93 | endScope: 'punctuation', 94 | 'on:begin': (match: { index: number }, response: CallbackResponse) => { 95 | response.data._beginIndex = match.index; 96 | }, 97 | 'on:end': (match: { input: string, index: number }, response: CallbackResponse) => { 98 | const stringBetweenBeginAndEnd: string = match.input.slice(response.data._beginIndex + 1, match.index); 99 | 100 | if (countChar(stringBetweenBeginAndEnd, '(') !== countChar(stringBetweenBeginAndEnd, ')')) { 101 | response.ignoreMatch(); 102 | } 103 | }, 104 | subLanguage: 'php', 105 | }; 106 | 107 | // x-data, x-init, x-show, x-on, etc. 108 | const ALPINE_JS_DIRECTIVES = { 109 | begin: /(?<=\sx-(?!transition|ref)[a-z0-9:.-]{2,}=)(?")/, 110 | excludeBegin: true, 111 | end: /(?")/, 112 | excludeEnd: true, 113 | subLanguage: 'javascript', 114 | } 115 | 116 | return { 117 | aliases: ['blade'], 118 | case_insensitive: false, 119 | subLanguage: 'xml', 120 | contains: [ 121 | hljs.COMMENT(/\{\{--/, /--}}/), 122 | ESCAPED_TEMPLATE_VARIABLE, 123 | UNESCAPED_TEMPLATE_VARIABLE, 124 | RAW_PHP, 125 | ALPINE_JS_STATEMENT_AFTER_X_BIND_SHORTHAND_SYNTAX, 126 | BLADE_COMPONENT_ATTRIBUTE, 127 | ALPINE_JS_X_ON_SHORTHAND_SYNTAX, 128 | ALPINE_JS_STATEMENT_AFTER_X_ON_SHORTHAND_SYNTAX, 129 | BLADE_DIRECTIVES, 130 | STATEMENT_AFTER_BLADE_DIRECTIVES, 131 | ALPINE_JS_DIRECTIVES, 132 | ], 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /tests/highlight.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from 'vitest'; 2 | import hljs from 'highlight.js/lib/core'; 3 | import javascript from 'highlight.js/lib/languages/javascript'; 4 | import xml from 'highlight.js/lib/languages/xml'; 5 | import php from 'highlight.js/lib/languages/php'; 6 | import blade from '../src/index.js'; 7 | 8 | hljs.registerLanguage('javascript', javascript); 9 | hljs.registerLanguage('xml', xml); 10 | hljs.registerLanguage('blade', blade); 11 | hljs.registerLanguage('php', php); 12 | 13 | describe('highlight laravel blade template', () => { 14 | it('should highlight the @if directive as keyword', () => { 15 | const code = ` 16 | @if ($isTrue) 17 |

Yes

18 | @else 19 |

No

20 | @endif 21 | `; 22 | const result = hljs.highlightAuto(code, ['blade']); 23 | 24 | expect(result.value) 25 | .to.contain('@if') 26 | .to.contain('$isTrue') 27 | .to.contain('@else') 28 | .to.contain('@endif'); 29 | }); 30 | 31 | it('should highlight the @for directive as keyword', () => { 32 | const code = ` 33 | @for ($i = 0; $i < 10; $i++) 34 |

{{ $i }}

35 | @endfor 36 | `; 37 | const result = hljs.highlightAuto(code, ['blade']); 38 | 39 | expect(result.value) 40 | .to.contain('@for') 41 | .to.contain('@endfor'); 42 | }); 43 | 44 | it('should highlight the @foreach directive as keyword', () => { 45 | const code = ` 46 | @foreach ($items as $item) 47 |

{{ $item }}

48 | @endforeach 49 | `; 50 | const result = hljs.highlightAuto(code, ['blade']); 51 | 52 | expect(result.value) 53 | .to.contain('@foreach') 54 | .to.contain('@endforeach'); 55 | }); 56 | 57 | it('should highlight the @while directive as keyword', () => { 58 | const code = ` 59 | @while ($i < 10) 60 |

{{ $i }}

61 | $i++; 62 | @endwhile 63 | `; 64 | const result = hljs.highlightAuto(code, ['blade']); 65 | 66 | expect(result.value) 67 | .to.contain('@while') 68 | .to.contain('@endwhile'); 69 | }); 70 | 71 | it('should highlight the escape template variable', () => { 72 | const code = '{{ $i }}'; 73 | const result = hljs.highlightAuto(code, ['blade']); 74 | 75 | expect(result.value) 76 | .to.contain('{{') 77 | .to.contain(' $i ') 78 | .to.contain('}}'); 79 | }); 80 | 81 | it('should highlight the unescape template variable', () => { 82 | const code = '{!! $i !!}'; 83 | const result = hljs.highlightAuto(code, ['blade']); 84 | 85 | expect(result.value) 86 | .to.contain('{!!') 87 | .to.contain(' $i ') 88 | .to.contain('!!}'); 89 | }); 90 | 91 | it('should highlight the statement after @use directive', () => { 92 | const code = "@use('App\\Models\\Flight')\n"; 93 | const result = hljs.highlightAuto(code, ['blade']); 94 | 95 | expect(result.value) 96 | .to.contain('@use') 97 | .to.contain(''App\\Models\\Flight''); 98 | }); 99 | 100 | it('should highlight the statement after @class directive', () => { 101 | const code = "@class(['p-1', 'bg-gray-100' => $active, 'bg-gray-200' => !$active])"; 102 | const result = hljs.highlightAuto(code, ['blade']); 103 | 104 | expect(result.value) 105 | .to.contain('@class') 106 | .to.contain('['p-1', 'bg-gray-100' => $active, 'bg-gray-200' => !$active]'); 107 | }); 108 | 109 | it('should highlight the statement that contain parentheses', () => { 110 | const code = "@selected(old('version') == $version)"; 111 | const result = hljs.highlightAuto(code, ['blade']); 112 | 113 | expect(result.value) 114 | .to.contain('@selected') 115 | .to.contain('@selected(old('version') == $version)'); 116 | }); 117 | 118 | it('should highlight the x-data and javascript statement', () => { 119 | const code = '
'; 120 | const result = hljs.highlightAuto(code, ['blade']); 121 | 122 | expect(result.value) 123 | .to.contain('x-data') 124 | .to.contain('{ open: false, toggle() { this.open = ! this.open } }'); 125 | }); 126 | 127 | it('should highlight the string after x-transition', () => { 128 | const code = '
'; 129 | const result = hljs.highlightAuto(code, ['blade']); 130 | 131 | expect(result.value) 132 | .to.contain('x-transition') 133 | .to.contain('"transition ease-out duration-300"'); 134 | }); 135 | 136 | it('should highlight the x-on shorthand syntax and javascript statement', () => { 137 | const code = ''; 138 | const result = hljs.highlightAuto(code, ['blade']); 139 | 140 | expect(result.value) 141 | .to.contain('@click') 142 | .to.contain('open = ! open'); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Highlight Blade 7 | 9 | 10 | 11 | 13 | 14 | 39 | 40 | 41 |
42 |

Examples of Highlight Blade

43 | 44 |

Highlight Laravel Blade Template with highlight.js.

45 | 46 |

Blade Template

47 | 48 |

# Basic

49 | 50 |
 51 |         @php
 52 |   $inIndexPage = request()->url() === route('posts.index');
 53 | @endphp
 54 | 
 55 | <a
 56 |   href="{{ route('posts.index') }}"
 57 |   @class([
 58 |       'flex items-center ...',
 59 |       'bg-gray-200 text-gray-900 ...' => $inIndexPage,
 60 |       'text-gray-500 dark:text-gray-400 ...' => !$inIndexPage,
 61 |   ])
 62 |   wire:navigate
 63 | >
 64 |   <x-icon.home class="w-4" />
 65 |   <span class="ml-2">All Posts</span>
 66 | </a>
 67 | 
 68 | <x-alert type="error" :message="$message" class="mt-4"/>
 69 |     
70 | 71 |

# If Condition

72 | 73 |
 74 |         @if ($attributes->has('class'))
 75 |     <div>Class attribute is present</div>
 76 | @endif
 77 |     
78 | 79 |

# If Else Condition

80 | 81 |
 82 |         @if (count($records) === 1)
 83 |     I have one record!
 84 | @elseif (count($records) > 1)
 85 |     I have multiple records!
 86 | @else
 87 |     I don't have any records!
 88 | @endif
 89 |     
90 | 91 |

# Foreach Condition

92 | 93 |
 94 |         <select name="version">
 95 |     @foreach ($product->versions as $version)
 96 |         <option value="{{ $version }}" @selected(old('version') == $version)>
 97 |             {{ $version }}
 98 |         </option>
 99 |     @endforeach
100 | </select>
101 |     
102 | 103 |

# Use Directive

104 | 105 |
106 |         @use('App\Models\Flight')
107 |     
108 | 109 |

Alpine.js

110 | 111 |

# Basic

112 | 113 |
114 |         <div x-data="{ open: false, toggle() { this.open = ! this.open } }">
115 |     <button @click="toggle()">Toggle Content</button>
116 | 
117 |     <div x-show="open">
118 |         Content...
119 |     </div>
120 | </div>
121 |     
122 | 123 |

# x-init

124 | 125 |
126 |         <div
127 |     x-data="{ posts: [] }"
128 |     x-init="posts = await (await fetch('/posts')).json()"
129 | >...</div>
130 |     
131 | 132 |

# x-bind

133 | 134 |
135 |         <div x-data="{ placeholder: 'Type here...' }">
136 |     <input type="text" x-bind:placeholder="placeholder">
137 | </div>
138 |     
139 | 140 |

# x-bind Shorthand Syntax (In Laravel Blade)

141 | 142 |
143 |         <div x-data="{ open: false }">
144 |     <button x-on:click="open = ! open">Toggle Dropdown</button>
145 | 
146 |     <div ::class="open ? '' : 'hidden'">
147 |         Dropdown Contents...
148 |     </div>
149 | </div>
150 |     
151 | 152 |

# x-on

153 | 154 |
155 |         <button x-on:click="alert('Hello World!')">Say Hi</button>
156 |     
157 | 158 |

# x-on Shorthand Syntax

159 | 160 |
161 |         <button type="button"
162 |     @click="message = 'selected'"
163 |     @click.shift="message = 'added to selection'"
164 |     @mousemove.shift="message = 'add to selection'"
165 |     @mouseout="message = 'select'"
166 |     x-text="message"
167 | >
168 | </button>
169 |     
170 | 171 |

# x-for

172 | 173 |
174 |         <ul x-data="{ colors: ['Red', 'Orange', 'Yellow'] }">
175 |     <template x-for="color in colors">
176 |         <li x-text="color"></li>
177 |     </template>
178 | </ul>
179 |     
180 | 181 |

# x-transition

182 | 183 |
184 |         <div x-data="{ open: false }">
185 |     <button @click="open = ! open">Toggle</button>
186 | 
187 |     <div
188 |         x-show="open"
189 |         x-transition:enter="transition ease-out duration-300"
190 |         x-transition:enter-start="opacity-0 scale-90"
191 |         x-transition:enter-end="opacity-100 scale-100"
192 |         x-transition:leave="transition ease-in duration-300"
193 |         x-transition:leave-start="opacity-100 scale-100"
194 |         x-transition:leave-end="opacity-0 scale-90"
195 |     >Hello 👋</div>
196 | </div>
197 |     
198 | 199 |

# x-ref

200 | 201 |
202 |         <button @click="$refs.text.remove()">Remove Text</button>
203 | 
204 | <span x-ref="text">Hello 👋</span>
205 |     
206 |
207 | 208 | 215 | 216 | --------------------------------------------------------------------------------