├── .babelrc ├── .browserslistrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmessage ├── .node-version ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── @types └── global.d.ts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── demo └── index.html ├── dist ├── class │ ├── HTMLCodeBlockElement.d.ts │ └── HTMLCodeBlockElement.js ├── effects │ ├── add-style.d.ts │ └── add-style.js ├── index.all.d.ts ├── index.all.js ├── index.common.d.ts ├── index.common.js ├── index.core.d.ts ├── index.core.js ├── manual.d.ts ├── manual.js ├── stylesheet.d.ts ├── stylesheet.js └── utils │ ├── createHighlightCallback.d.ts │ └── createHighlightCallback.js ├── jest.config.ts ├── lib ├── html-code-block-element.all.min.js ├── html-code-block-element.all.min.licenses.txt ├── html-code-block-element.common.min.js ├── html-code-block-element.common.min.licenses.txt ├── html-code-block-element.core.min.js └── html-code-block-element.core.min.licenses.txt ├── package-lock.json ├── package.json ├── src ├── class │ └── HTMLCodeBlockElement.ts ├── effects │ └── add-style.ts ├── index.all.ts ├── index.common.ts ├── index.core.ts ├── manual.ts ├── stylesheet.ts └── utils │ └── createHighlightCallback.ts ├── test ├── HTMLCodeBlockElement.test.ts └── HTMLCodeBlockElement.throw.test.ts ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env"], ["@babel/preset-typescript"]], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | not IE 11 3 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' # required to adjust maintainability checks 2 | exclude_patterns: 3 | - '**/*.test.ts' 4 | - '**/*.js' 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | 5 | charset = utf-8 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["google", "plugin:@typescript-eslint/recommended", "prettier"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": { 14 | "max-len": "off", 15 | "no-unused-vars": "off", 16 | "no-invalid-this": "off", 17 | "require-jsdoc": "off", 18 | "valid-jsdoc": [ 19 | "error", 20 | { 21 | "requireReturn": false, 22 | "requireParamType": false, 23 | "requireReturnType": false 24 | } 25 | ], 26 | "lines-between-class-members": ["error", "always"] 27 | }, 28 | "overrides": [ 29 | { 30 | "files": ["./webpack.config.js"], 31 | "rules": { 32 | "@typescript-eslint/no-var-requires": "off" 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: 5 | - main 6 | - feature/* 7 | - release/* 8 | paths: 9 | - '**.ts' 10 | - '**.json' 11 | - '**.yml' 12 | pull_request: 13 | paths: 14 | - '!*.md' 15 | jobs: 16 | test: 17 | strategy: 18 | matrix: 19 | # platform: [ windows-latest, macOS-latest ] 20 | platform: [ubuntu-latest] 21 | node: ['16'] 22 | name: Code check on Node.${{ matrix.node }}/${{ matrix.platform }} 23 | runs-on: ${{ matrix.platform }} 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v2 27 | with: 28 | node-version: '16' 29 | - run: npm ci 30 | - run: npm test 31 | - uses: paambaati/codeclimate-action@v2.7.5 32 | env: 33 | CC_TEST_REPORTER_ID: af882de8052c9e436d8e7980bec9c61773f9a9e8cf327b27e97832bfa9628f11 34 | with: 35 | coverageLocations: | 36 | ${{github.workspace}}/coverage/lcov.info:lcov 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.gitmessage: -------------------------------------------------------------------------------- 1 | # ==== Emojis ==== 2 | # ✨ :sparkles: A new feature(新機能) 3 | # 🐛 :bug: Bug fixes(バグ修正) 4 | # 📚 :books: Change document only(ドキュメント更新) 5 | # 🐎 :racehorse: Changes to improve performance(パフォーマンスチューニング) 6 | # 🚿 :shower: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)(コードの処理が変わらない変更) 7 | # ♻️ :recycle: Changes that are not bug fixes or feature additions (リファクタリング) 8 | # 🚨 :rotating_light: Adding missing or correcting existing tests(テストコードまわり) 9 | # 🔧 :wrench: Changes to the build process, package.json, etc.(開発環境、CIなど) 10 | # 🗑 :wastebasket: Committing to repeal, delete, etc.(削除) 11 | # ⚠️ :warning: Important commits for quality and stable(重要なコミット) 12 | # ⚡️ :zap: Other changes(その他の更新) 13 | # ⚙ :gear: Only build(ビルドコマンド実行結果) 14 | 15 | # ==== Commit comment example ==== 16 | # :zap: Subject text 17 | # 18 | # Commit comment body... 19 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v16.13.0 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | heading="🥒" 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | dist/ 3 | node_modules/ 4 | *.html 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": false, 4 | "bracketSpacing": false, 5 | "embeddedLanguageFormatting": "auto", 6 | "htmlWhitespaceSensitivity": "css", 7 | "insertPragma": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 80, 10 | "proseWrap": "preserve", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": true, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "es5", 17 | "useTabs": false, 18 | "vueIndentScriptAndStyle": false 19 | } 20 | -------------------------------------------------------------------------------- /@types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react' { 2 | // A type for the properties of a function component 3 | interface CodeBlockHTMLAttributes extends HTMLAttributes { 4 | /** The accessible name of code block */ 5 | label?: string | undefined; 6 | /** The Language name */ 7 | language?: string | undefined; 8 | /** The flag to display the UI */ 9 | controls?: boolean; 10 | } 11 | } 12 | 13 | export type CodeBlockProps = React.DetailedHTMLProps< 14 | React.HTMLAttributes, 15 | HTMLPreElement 16 | > & { 17 | /** The accessible name of code block */ 18 | label?: string | undefined; 19 | /** The Language name */ 20 | language?: string | undefined; 21 | /** The flag to display the UI */ 22 | controls?: boolean; 23 | }; 24 | 25 | declare global { 26 | // A type for JSX markup 27 | namespace JSX { 28 | interface IntrinsicElements { 29 | 'code-block': CodeBlockProps; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | - The test of `value` props 11 | - The test of `textContent` props 12 | 13 | ## [2.0.4] - 2023-05-21 14 | 15 | - Update supporting for usage on React 16 | - Add data-id on the style element 17 | 18 | ## [2.0.0] - 2022-12-22 19 | 20 | - Update node version 21 | - Update README.md 22 | - Update demo HTML 23 | - Add `notrim` attribute 24 | - Add prettier 25 | 26 | ## [1.1.1] - 2021-09-06 27 | 28 | ### Fixed 29 | 30 | - Fix exmaples on README.md 31 | 32 | ## [1.1.0] - 2021-09-06 33 | 34 | ### Fixed 35 | 36 | - Fix a few code according to the coding rules. 37 | - Fix the bug of ignoring `null` when the `value` of `label` and `language` are empty. 38 | 39 | ### Added 40 | 41 | - [CHANGELOG.md](CHANGELOG.md) 42 | - Support to edit by `textContent` propperty 43 | - Support to use in React 44 | 45 | ### Update 46 | 47 | - Reduce the number of unnecessary calls of `#render` 48 | 49 | ### Remove 50 | 51 | - The test of `value` props 52 | - The test of `textContent` props 53 | 54 | ## [1.0.7] - 2021-09-05 55 | 56 | ### Fixed 57 | 58 | - README.md 59 | 60 | ## [1.0.6] - 2021-08-08 61 | 62 | ### Update 63 | 64 | - README.md 65 | 66 | ## [1.0.5] - 2021-08-08 67 | 68 | ### Update 69 | 70 | - README.md 71 | 72 | ## [1.0.4] - 2021-08-08 73 | 74 | ### Added 75 | 76 | - bower.json 77 | 78 | ## [1.0.3] - 2021-08-02 79 | 80 | ### Fixed 81 | 82 | - main prop in package.json 83 | 84 | ## [1.0.1] - 2021-08-02 85 | 86 | ### Fixed 87 | 88 | - main prop in package.json 89 | 90 | ## [1.0.0] - 2021-08-02 91 | 92 | ### Added 93 | 94 | - Linter 95 | - Test 96 | 97 | ## [0.2.1] - 2021-08-01 98 | 99 | ### Fixed 100 | 101 | - Handling blank lines 102 | 103 | ## [0.2.0] - 2021-08-01 104 | 105 | ### Added 106 | 107 | - Copy button 108 | 109 | ## [0.1.0] - 2021-08-01 110 | 111 | ### Added 112 | 113 | - The `label` attribute 114 | 115 | ### Update 116 | 117 | - Default style 118 | - Refactoring 119 | 120 | ## [0.0.1] - 2021-07-31 121 | 122 | ### Added 123 | 124 | - First release 125 | 126 | [1.1.0]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v1.1.0 127 | [1.0.7]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v1.0.7 128 | [1.0.6]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v1.0.6 129 | [1.0.5]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v1.0.5 130 | [1.0.4]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v1.0.4 131 | [1.0.3]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v1.0.3 132 | [1.0.1]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v1.0.1 133 | [1.0.0]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v1.0.0 134 | [0.2.1]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v0.2.1 135 | [0.2.0]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v0.2.0 136 | [0.1.0]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v0.1.0 137 | [0.0.1]: https://github.com/heppokofrontend/html-code-block-element/releases/tag/v0.0.1 138 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork it! 4 | 2. Create your feature branch: `git switch -c my-new-feature` 5 | 3. Commit your changes: `git commit -am 'Add some feature'` 6 | 4. Push to the branch: `git push origin my-new-feature` 7 | 5. Submit a pull request :D 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 html-code-block-element 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # <code-block> 2 | 3 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) [![Published on NPM](https://img.shields.io/npm/v/@heppokofrontend/html-code-block-element.svg)](https://www.npmjs.com/package/@heppokofrontend/html-code-block-element) [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/heppokofrontend/html-code-block-element) [![](https://data.jsdelivr.com/v1/package/npm/@heppokofrontend/html-code-block-element/badge)](https://www.jsdelivr.com/package/npm/@heppokofrontend/html-code-block-element) [![Maintainability](https://api.codeclimate.com/v1/badges/38a4e238adb7401844ba/maintainability)](https://codeclimate.com/github/heppokofrontend/html-code-block-element/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/38a4e238adb7401844ba/test_coverage)](https://codeclimate.com/github/heppokofrontend/html-code-block-element/test_coverage) [![Known Vulnerabilities](https://snyk.io/test/npm/@heppokofrontend/html-code-block-element/badge.svg)](https://snyk.io/test/npm/@heppokofrontend/html-code-block-element) 4 | [![@heppokofrontend/html-code-block-element](https://snyk.io/advisor/npm-package/@heppokofrontend/html-code-block-element/badge.svg)](https://snyk.io/advisor/npm-package/@heppokofrontend/html-code-block-element) 5 | 6 | Code block custom element with syntax highlighting and copy button. 7 | 8 | It has [highlight.js](https://www.npmjs.com/package/highlight.js?activeTab=readme) for syntax highlighting. 9 | 10 | ## Usage 11 | 12 | DEMO: 13 | 14 | 25 | 26 | ```html 27 | 28 | <script>console.log(true);</script> 29 | 30 | ``` 31 | 32 | ### In browser 33 | 34 | It can be used by loading html-code-block-element.common.min.js and one CSS theme. 35 | 36 | The highlight.js style is available on CDN. You can also download the JS and CSS from the respective repositories and load them into your page. 37 | 38 | ```html 39 | 43 | 47 | ``` 48 | 49 | There are three versions of this library available. 50 | 51 | - `html-code-block-element.common.min.js` - One that supports only the popular languages. 52 | - `html-code-block-element.all.min.js` - One that enables [all languages](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md) supported by highligh.js. 53 | - `html-code-block-element.core.min.js` - For developers who want to do their own `hljs.registerLanguage()`. 54 | 55 | #### Example 56 | 57 | **Note:** The textarea element cannot be included in the content of the textarea element. If you want to include it, please use HTML Entity for the tag. 58 | 59 | ```html 60 | 61 | 62 | 63 | ``` 64 | 65 | or 66 | 67 | ```html 68 | 69 | <script>console.log(true);</script> 70 | 71 | ``` 72 | 73 | #### Assumption specifications 74 | 75 | - **Categories:** 76 | - [Flow content](https://html.spec.whatwg.org/multipage/dom.html#flow-content-2). 77 | - [Palpable content](https://html.spec.whatwg.org/multipage/dom.html#palpable-content-2). 78 | - **Contexts in which this element can be used:** 79 | - Where flow content is expected. 80 | - **Content model:** 81 | - [Text](https://html.spec.whatwg.org/multipage/dom.html#text-content) or one [textarea](https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element) element 82 | - **Tag omission in text/html:** 83 | - Neither tag is omissible. 84 | - **Content attributes:** 85 | - [Global attributes](https://html.spec.whatwg.org/multipage/dom.html#global-attributes) 86 | - `controls` - Show controls 87 | - `notrim` - Does't remove whitespace from both ends of a source 88 | - `label` - Give the code block a unique name. If omitted, it will always have the accessible name "Code Block". 89 | - `language` - Language name of the code. If omitted, it will be detected automatically. 90 | - **Accessibility considerations:** 91 | - [No corresponding role](https://w3c.github.io/html-aria/#dfn-no-corresponding-role) 92 | - `role` attribute is not allowed 93 | - `aria-*` attribute is not allowed 94 | 95 | #### DOM interface 96 | 97 | ```java 98 | [Exposed=Window] 99 | interface HTMLCodeBlockElement : HTMLElement { 100 | [HTMLConstructor] constructor(); 101 | 102 | [CEReactions] attribute boolean controls; 103 | [CEReactions] attribute boolean notrim; 104 | [CEReactions] attribute DOMString label; 105 | [CEReactions] attribute DOMString language; 106 | [CEReactions] attribute DOMString value; 107 | }; 108 | ``` 109 | 110 | ### In development 111 | 112 | #### Installation: 113 | 114 | ```shell 115 | npm install --save @heppokofrontend/html-code-block-element 116 | ``` 117 | 118 | #### Usage: 119 | 120 | The `customElements.define()` will also be included. 121 | 122 | ```javascript 123 | // For highlighting, `highlight.js/lib/common` will be used. 124 | import '@heppokofrontend/html-code-block-element'; 125 | // For highlighting, `highlight.js` will be used. 126 | import '@heppokofrontend/html-code-block-element/dist/index.all'; 127 | // For highlighting, `highlight.js/lib/code` will be used. 128 | import '@heppokofrontend/html-code-block-element/dist/index.core'; 129 | ``` 130 | 131 | If you are using purely constructors: 132 | 133 | ```javascript 134 | import HTMLCodeBlockElement from '@heppokofrontend/html-code-block-element/dist/class/HTMLCodeBlockElement'; 135 | ``` 136 | 137 | #### Use in React 138 | 139 | This package contains the global type files for React. 140 | 141 | - `React.CodeBlockHTMLAttributes` 142 | - `code-block` in `JSX.IntrinsicElements` 143 | 144 | \* CSS needs to be loaded separately. 145 | 146 | ```tsx 147 | // CodeBlock.tsx 148 | import {CodeBlockProps} from '@heppokofrontend/html-code-block-element/dist/manual'; 149 | import styleSheet from '@heppokofrontend/html-code-block-element/dist/styleSheet'; 150 | import hljs from 'highlight.js/lib/common'; 151 | import Head from 'next/head'; 152 | import {useEffect} from 'react'; 153 | 154 | declare module 'react' { 155 | // A type for the properties of a function component 156 | interface CodeBlockHTMLAttributes extends HTMLAttributes { 157 | /** The accessible name of code block */ 158 | label?: string | undefined; 159 | /** The Language name */ 160 | language?: string | undefined; 161 | /** The flag to display the UI */ 162 | controls?: boolean; 163 | } 164 | } 165 | 166 | declare global { 167 | // A type for JSX markup 168 | namespace JSX { 169 | interface IntrinsicElements { 170 | 'code-block': CodeBlockProps; 171 | } 172 | } 173 | } 174 | 175 | type Props = Omit, 'className'>; 176 | 177 | let isLoaded = false; 178 | 179 | export const CodeBlock = ({children, ...props}: Props) => { 180 | useEffect(() => { 181 | const loadWebComponent = async () => { 182 | const {HTMLCodeBlockElement, createHighlightCallback} = await import( 183 | '@heppokofrontend/html-code-block-element/dist/manual' 184 | ); 185 | 186 | HTMLCodeBlockElement.highlight = createHighlightCallback(hljs); 187 | customElements.define('code-block', HTMLCodeBlockElement); 188 | }; 189 | 190 | if (!isLoaded) { 191 | isLoaded = true; 192 | loadWebComponent(); 193 | } 194 | }, []); 195 | 196 | return ( 197 | <> 198 | 199 | 200 | 201 | {children} 202 | 203 | ); 204 | }; 205 | ``` 206 | 207 | #### Use as constructor 208 | 209 | Manual setup: 210 | 211 | ```js 212 | // 1. Import 213 | import hljs from 'highlight.js/lib/core'; 214 | import javascript from 'highlight.js/lib/languages/javascript'; 215 | import HTMLCodeBlockElement from '@heppokofrontend/html-code-block-element/dist/class/HTMLCodeBlockElement'; 216 | // or import { HTMLCodeBlockElement } from '@heppokofrontend/html-code-block-element'; 217 | 218 | // Support JavaScript 219 | hljs.registerLanguage('javascript', javascript); 220 | 221 | // 2. Set endgine 222 | /** 223 | * Example: Callback to be called internally 224 | * @param {string} src - Source code string for highlight 225 | * @param {{ language: string }} options - Option for highlight 226 | * @returns {{ markup: string }} - Object of the highlight result 227 | */ 228 | HTMLCodeBlockElement.highlight = function (src, options) { 229 | if ( 230 | // Verifying the existence of a language 231 | options?.language && 232 | hljs.getLanguage(options.language) 233 | ) { 234 | const {value} = hljs.highlight(src, options); 235 | 236 | return { 237 | markup: value, 238 | }; 239 | } 240 | 241 | return { 242 | markup: hljs.highlightAuto(src).value, 243 | }; 244 | }; 245 | 246 | // 3. Define 247 | customElements.define('code-block', HTMLCodeBlockElement); 248 | 249 | // 4. Make 250 | const cbElm = new HTMLCodeBlockElement(); 251 | 252 | // 5. Assign 253 | cbElm.language = 'javascript'; 254 | cbElm.label = 'your label'; 255 | cbElm.value = `const hoge = true; 256 | 257 | console.log(hoge);`; 258 | 259 | // 6. Append 260 | document.body.append(cbElm); // Render at the same time 261 | ``` 262 | 263 | ##### Syntax 264 | 265 | No params. 266 | 267 | ```js 268 | new HTMLCodeBlockElement(); 269 | ``` 270 | 271 | ## Support browser 272 | 273 | - Chrome 274 | - Safari 275 | - Firefox 276 | - Edge 277 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-code-block-element", 3 | "license": "https://github.com/heppokofrontend/html-code-block-element/blob/main/LICENSE" 4 | } 5 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HTMLCodeBlockElement 8 | 9 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |

HTMLCodeBlockElement

65 | 66 |

Code block custom element with syntax highlighting and copy button.

67 | 68 |

It is recommended that the following CSS be loaded for before JavaScript works

69 | 70 | 71 | 91 | 92 | 93 |

With escaped text

94 | 95 |

Source

96 | 97 | 98 | 101 | 102 | 103 |

Result

104 | 105 | 106 | <script>console.log(true);</script> 107 | 108 | 109 |

With the textarea element

110 | 111 |

Source

112 | 113 |

Note: The textarea element cannot be included in the content of the textarea element. If you want to include it, please use HTML Entity for the tag.

114 | 115 | 116 | 119 | 120 | 121 |

Result

122 | 123 | 124 | 125 | 126 | 127 |

Playground

128 | 129 | // this is placeholder (code-block#cb) 130 | 131 |

Let's paste these in conosole to run JS on this page.

132 | 133 | 134 | 142 | 143 | 144 | 145 | 152 | 153 | 154 | 155 | 165 | 166 |
167 | 170 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /dist/class/HTMLCodeBlockElement.d.ts: -------------------------------------------------------------------------------- 1 | export declare type EndgineProps = { 2 | src: string; 3 | options?: { 4 | /** Language Mode Name */ 5 | language: string; 6 | }; 7 | }; 8 | export declare type EndgineFunction = (props: EndgineProps) => { 9 | markup: string; 10 | }; 11 | export default class HTMLCodeBlockElement extends HTMLElement { 12 | #private; 13 | /** 14 | * Returns the result of highlighting the received source code string. 15 | * Before running `customElements.define()`, 16 | * you need to assign it directly to `HTMLCodeBlockElement.highlight`. 17 | */ 18 | static get highlight(): EndgineFunction; 19 | static set highlight(endgine: EndgineFunction); 20 | /** @return - Syntax Highlighted Source Code */ 21 | get value(): string; 22 | set value(src: unknown); 23 | /** 24 | * The accessible name of code block 25 | * @return - The value of the label attribute 26 | */ 27 | get label(): string; 28 | set label(value: unknown); 29 | /** 30 | * Language name 31 | * @return - The value of the language attribute 32 | */ 33 | get language(): string; 34 | set language(value: unknown); 35 | /** 36 | * The flag to display the UI 37 | * @return - With or without controls attribute 38 | * */ 39 | get controls(): boolean; 40 | set controls(value: boolean); 41 | set notrim(value: boolean); 42 | static get observedAttributes(): string[]; 43 | attributeChangedCallback(attrName: string, oldValue: string, newValue: string): void; 44 | connectedCallback(): void; 45 | constructor(); 46 | } 47 | -------------------------------------------------------------------------------- /dist/class/HTMLCodeBlockElement.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class HTMLCodeBlockElement extends HTMLElement { 4 | static #defaultEndgine = ({ src }) => ({ 5 | markup: src, 6 | }); 7 | static #endgine = HTMLCodeBlockElement.#defaultEndgine; 8 | /** 9 | * Returns the result of highlighting the received source code string. 10 | * Before running `customElements.define()`, 11 | * you need to assign it directly to `HTMLCodeBlockElement.highlight`. 12 | */ 13 | static get highlight() { 14 | const endgine = HTMLCodeBlockElement.#endgine; 15 | if (endgine === HTMLCodeBlockElement.#defaultEndgine) { 16 | throw new TypeError('The syntax highlighting engine is not set to `HTMLCodeBlockElement.highlight`.'); 17 | } 18 | return endgine; 19 | } 20 | static set highlight(endgine) { 21 | HTMLCodeBlockElement.#endgine = endgine; 22 | } 23 | static #createSlotElement = ({ name, id = '', }) => { 24 | const slot = document.createElement('slot'); 25 | slot.name = name; 26 | if (id) { 27 | slot.id = id; 28 | } 29 | return slot; 30 | }; 31 | /** Observer to monitor the editing of the content of this element. */ 32 | #observer = new MutationObserver(() => { 33 | this.#observer.disconnect(); 34 | // Remove elements other than element with `code` as `slot` attribute value. 35 | // The content of the `[slot="code"]` element will be passed to next rendering. 36 | const slots = this.querySelectorAll('[slot]:not([slot="code"])'); 37 | for (const slot of slots) { 38 | slot.remove(); 39 | } 40 | this.#value = (this.textContent || this.getAttribute('value') || '') 41 | .replace(/^\n/, '') 42 | .replace(/\n$/, ''); 43 | this.#render(); 44 | }); 45 | /** Slot elements for Shadow DOM content */ 46 | #slots = { 47 | name: HTMLCodeBlockElement.#createSlotElement({ name: 'name', id: 'name' }), 48 | copyButton: HTMLCodeBlockElement.#createSlotElement({ name: 'copy-button' }), 49 | code: HTMLCodeBlockElement.#createSlotElement({ name: 'code' }), 50 | }; 51 | /** 52 | * True when rendered at least once. 53 | * The purpose of this flag is to available the operation the following usage. 54 | * 55 | * Specifically, this is the case where an element is rendered 56 | * on the screen without ever using the value property. 57 | * 58 | * ```js 59 | * const cb = document.createElement('code-block'); 60 | * 61 | * cb.language = 'json'; 62 | * cb.textContent = '{"a": 100}'; 63 | * document.body.prepend(cb); 64 | * ``` 65 | */ 66 | #rendered = false; 67 | /** Pure DOM content */ 68 | #a11yName; 69 | /** Pure DOM content */ 70 | #copyButton; 71 | /** Pure DOM content */ 72 | #codeBlock; 73 | /** Pure DOM content */ 74 | #codeWrap; 75 | /** Actual value of the accessor `value` */ 76 | #value = ''; 77 | /** Actual value of the accessor `label` */ 78 | #label = ''; 79 | /** Actual value of the accessor `language` */ 80 | #language = ''; 81 | /** Actual value of the accessor `controls` */ 82 | #controls = false; 83 | /** Actual value of the accessor `notrim` */ 84 | #notrim = false; 85 | /** Click event handler of copy button */ 86 | #onClickButton = (() => { 87 | let key = -1; 88 | /** 89 | * Write to the clipboard. 90 | * @param value - String to be saved to the clipboard 91 | * @return - A promise 92 | */ 93 | const copy = (value) => { 94 | if (navigator.clipboard) { 95 | return navigator.clipboard.writeText(value); 96 | } 97 | return new Promise((r) => { 98 | const textarea = document.createElement('textarea'); 99 | textarea.value = value; 100 | document.body.append(textarea); 101 | textarea.select(); 102 | document.execCommand('copy'); 103 | textarea.remove(); 104 | r(); 105 | }); 106 | }; 107 | return async () => { 108 | const value = this.#value.replace(/\n$/, ''); 109 | clearTimeout(key); 110 | await copy(value); 111 | this.#copyButton.classList.add('--copied'); 112 | this.#copyButton.textContent = 'Copied!'; 113 | key = window.setTimeout(() => { 114 | this.#copyButton.classList.remove('--copied'); 115 | this.#copyButton.textContent = 'Copy'; 116 | }, 1500); 117 | }; 118 | })(); 119 | /** Outputs the resulting syntax-highlighted markup to the DOM. */ 120 | #render() { 121 | if (!this.parentNode) { 122 | return; 123 | } 124 | this.#observer.disconnect(); 125 | const src = this.#notrim ? this.#value : this.#value.trim(); 126 | /** The resulting syntax-highlighted markup */ 127 | const { markup } = HTMLCodeBlockElement.highlight({ 128 | src, 129 | options: { 130 | language: this.#language, 131 | }, 132 | }); 133 | // initialize 134 | this.textContent = ''; 135 | this.#a11yName.textContent = this.#label; 136 | this.#slots.name.hidden = !this.#label; 137 | this.#slots.copyButton.hidden = !this.#controls; 138 | this.#codeBlock.textContent = ''; 139 | this.#codeBlock.insertAdjacentHTML('afterbegin', markup); 140 | this.append(this.#a11yName); 141 | this.append(this.#copyButton); 142 | this.append(this.#codeWrap); 143 | this.#observer.observe(this, { 144 | childList: true, 145 | }); 146 | } 147 | /** @return - Syntax Highlighted Source Code */ 148 | get value() { 149 | return this.#value; 150 | } 151 | set value(src) { 152 | this.#value = String(src); 153 | this.#render(); 154 | } 155 | /** 156 | * The accessible name of code block 157 | * @return - The value of the label attribute 158 | */ 159 | get label() { 160 | return this.#label; 161 | } 162 | set label(value) { 163 | if (this.#label === value || 164 | (this.#label === '' && 165 | this.getAttribute('label') === null && 166 | value === null)) { 167 | return; 168 | } 169 | if (value === null) { 170 | this.#label = ''; 171 | this.removeAttribute('label'); 172 | } 173 | else { 174 | this.#label = String(value); 175 | this.setAttribute('label', this.#label); 176 | } 177 | this.#render(); 178 | } 179 | /** 180 | * Language name 181 | * @return - The value of the language attribute 182 | */ 183 | get language() { 184 | return this.#language; 185 | } 186 | set language(value) { 187 | if (this.#language === value || 188 | (this.#language === '' && 189 | this.getAttribute('language') === null && 190 | value === null)) { 191 | return; 192 | } 193 | if (value === null) { 194 | this.#language = ''; 195 | this.removeAttribute('language'); 196 | } 197 | else { 198 | this.#language = String(value); 199 | this.setAttribute('language', this.#language); 200 | } 201 | this.#render(); 202 | } 203 | /** 204 | * The flag to display the UI 205 | * @return - With or without controls attribute 206 | * */ 207 | get controls() { 208 | return this.#controls; 209 | } 210 | set controls(value) { 211 | if (this.#controls === value) { 212 | return; 213 | } 214 | this.#controls = value; 215 | if (this.#controls) { 216 | this.setAttribute('controls', ''); 217 | } 218 | else { 219 | this.removeAttribute('controls'); 220 | } 221 | this.#render(); 222 | } 223 | set notrim(value) { 224 | if (this.#notrim === value) { 225 | return; 226 | } 227 | this.#notrim = value; 228 | if (this.#notrim) { 229 | this.setAttribute('notrim', ''); 230 | } 231 | else { 232 | this.removeAttribute('notrim'); 233 | } 234 | this.#render(); 235 | } 236 | static get observedAttributes() { 237 | return ['label', 'language', 'controls', 'notrim']; 238 | } 239 | attributeChangedCallback(attrName, oldValue, newValue) { 240 | if (oldValue === newValue) { 241 | return; 242 | } 243 | // When the value of the attribute being observed changes, 244 | // pass value to accessors. 245 | switch (attrName) { 246 | // string 247 | case 'label': 248 | case 'language': 249 | this[attrName] = newValue; 250 | break; 251 | // boolean 252 | case 'controls': 253 | case 'notrim': 254 | this[attrName] = typeof newValue === 'string'; 255 | } 256 | } 257 | connectedCallback() { 258 | if (this.#rendered === false && this.#value === '') { 259 | this.#value = this.textContent || ''; 260 | } 261 | this.#rendered = true; 262 | this.#render(); 263 | } 264 | constructor() { 265 | super(); 266 | /* ------------------------------------------------------------------------- 267 | * Setup DOM contents 268 | * ---------------------------------------------------------------------- */ 269 | /** Container of accessible names (label attribute values). */ 270 | const a11yName = (() => { 271 | const span = document.createElement('span'); 272 | span.slot = 'name'; 273 | span.textContent = this.getAttribute('label') || ''; 274 | return span; 275 | })(); 276 | const copyButton = (() => { 277 | const button = document.createElement('button'); 278 | button.type = 'button'; 279 | button.slot = 'copy-button'; 280 | button.textContent = 'Copy'; 281 | button.setAttribute('aria-live', 'polite'); 282 | button.addEventListener('click', this.#onClickButton); 283 | return button; 284 | })(); 285 | const codeElm = (() => { 286 | const code = document.createElement('code'); 287 | code.tabIndex = 0; 288 | code.className = 'hljs'; // TODO: Make it variable 289 | return code; 290 | })(); 291 | const preElm = (() => { 292 | const pre = document.createElement('pre'); 293 | pre.slot = 'code'; 294 | pre.append(codeElm); 295 | return pre; 296 | })(); 297 | /* ------------------------------------------------------------------------- 298 | * Setup Shadow DOM contents 299 | * ---------------------------------------------------------------------- */ 300 | /** 301 | * The container of minimum text that will be read even 302 | * if the accessible name (label attribute value) is omitted. 303 | */ 304 | const a11yNamePrefix = (() => { 305 | const span = document.createElement('span'); 306 | span.id = 'semantics'; 307 | span.hidden = true; 308 | span.textContent = 'Code Block'; 309 | return span; 310 | })(); 311 | const container = (() => { 312 | const div = document.createElement('div'); 313 | div.append(...Object.values(this.#slots)); 314 | div.setAttribute('role', 'group'); 315 | div.setAttribute('aria-labelledby', 'semantics name'); 316 | return div; 317 | })(); 318 | const shadowRoot = this.attachShadow({ 319 | mode: 'closed', 320 | }); 321 | shadowRoot.append(a11yNamePrefix); 322 | shadowRoot.append(container); 323 | /* ------------------------------------------------------------------------- 324 | * Hard private props initialize 325 | * ---------------------------------------------------------------------- */ 326 | this.#value = (this.textContent || '') 327 | .replace(/^\n/, '') 328 | .replace(/\n$/, ''); 329 | this.#label = a11yName.textContent || ''; 330 | this.#language = this.getAttribute('language') || ''; 331 | this.#controls = this.getAttribute('controls') !== null; 332 | this.#notrim = this.getAttribute('notrim') !== null; 333 | this.#a11yName = a11yName; 334 | this.#copyButton = copyButton; 335 | this.#codeBlock = codeElm; 336 | this.#codeWrap = preElm; 337 | } 338 | } 339 | exports.default = HTMLCodeBlockElement; 340 | // Protect constructor names from minify 341 | Object.defineProperty(HTMLCodeBlockElement, 'name', { 342 | value: 'HTMLCodeBlockElement', 343 | }); 344 | -------------------------------------------------------------------------------- /dist/effects/add-style.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/effects/add-style.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Inserts the style element into the page for 3 | var __importDefault = (this && this.__importDefault) || function (mod) { 4 | return (mod && mod.__esModule) ? mod : { "default": mod }; 5 | }; 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | const stylesheet_1 = __importDefault(require("../stylesheet")); 8 | // the default style of the code-block element. 9 | const style = document.createElement('style'); 10 | const link = document.querySelector('head link, head style'); 11 | style.textContent = stylesheet_1.default; 12 | style.dataset.id = 'html-code-block-element'; 13 | if (link) { 14 | link.before(style); 15 | } 16 | else { 17 | document.head.append(style); 18 | } 19 | -------------------------------------------------------------------------------- /dist/index.all.d.ts: -------------------------------------------------------------------------------- 1 | import CustomElementConstructor from './class/HTMLCodeBlockElement'; 2 | import './effects/add-style'; 3 | export declare const HTMLCodeBlockElement: typeof CustomElementConstructor; 4 | -------------------------------------------------------------------------------- /dist/index.all.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.HTMLCodeBlockElement = void 0; 7 | const highlight_js_1 = __importDefault(require("highlight.js")); 8 | const HTMLCodeBlockElement_1 = __importDefault(require("./class/HTMLCodeBlockElement")); 9 | const createHighlightCallback_1 = require("./utils/createHighlightCallback"); 10 | require("./effects/add-style"); 11 | HTMLCodeBlockElement_1.default.highlight = (0, createHighlightCallback_1.createHighlightCallback)(highlight_js_1.default); 12 | customElements.define('code-block', HTMLCodeBlockElement_1.default); 13 | exports.HTMLCodeBlockElement = HTMLCodeBlockElement_1.default; 14 | -------------------------------------------------------------------------------- /dist/index.common.d.ts: -------------------------------------------------------------------------------- 1 | import CustomElementConstructor from './class/HTMLCodeBlockElement'; 2 | import './effects/add-style'; 3 | export declare const HTMLCodeBlockElement: typeof CustomElementConstructor; 4 | -------------------------------------------------------------------------------- /dist/index.common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.HTMLCodeBlockElement = void 0; 7 | const common_1 = __importDefault(require("highlight.js/lib/common")); 8 | const HTMLCodeBlockElement_1 = __importDefault(require("./class/HTMLCodeBlockElement")); 9 | const createHighlightCallback_1 = require("./utils/createHighlightCallback"); 10 | require("./effects/add-style"); 11 | HTMLCodeBlockElement_1.default.highlight = (0, createHighlightCallback_1.createHighlightCallback)(common_1.default); 12 | customElements.define('code-block', HTMLCodeBlockElement_1.default); 13 | exports.HTMLCodeBlockElement = HTMLCodeBlockElement_1.default; 14 | -------------------------------------------------------------------------------- /dist/index.core.d.ts: -------------------------------------------------------------------------------- 1 | import CustomElementConstructor from './class/HTMLCodeBlockElement'; 2 | import './effects/add-style'; 3 | export declare const HTMLCodeBlockElement: typeof CustomElementConstructor; 4 | -------------------------------------------------------------------------------- /dist/index.core.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.HTMLCodeBlockElement = void 0; 7 | const core_1 = __importDefault(require("highlight.js/lib/core")); 8 | const HTMLCodeBlockElement_1 = __importDefault(require("./class/HTMLCodeBlockElement")); 9 | const createHighlightCallback_1 = require("./utils/createHighlightCallback"); 10 | require("./effects/add-style"); 11 | HTMLCodeBlockElement_1.default.highlight = (0, createHighlightCallback_1.createHighlightCallback)(core_1.default); 12 | customElements.define('code-block', HTMLCodeBlockElement_1.default); 13 | exports.HTMLCodeBlockElement = HTMLCodeBlockElement_1.default; 14 | -------------------------------------------------------------------------------- /dist/manual.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import CustomElementConstructor from './class/HTMLCodeBlockElement'; 3 | export declare type CodeBlockProps = React.DetailedHTMLProps, HTMLPreElement> & { 4 | /** The accessible name of code block */ 5 | label?: string | undefined; 6 | /** The Language name */ 7 | language?: string | undefined; 8 | /** The flag to display the UI */ 9 | controls?: boolean; 10 | }; 11 | export declare const HTMLCodeBlockElement: typeof CustomElementConstructor; 12 | export declare const createHighlightCallback: (endgine: import("highlight.js").HLJSApi) => import("./class/HTMLCodeBlockElement").EndgineFunction; 13 | -------------------------------------------------------------------------------- /dist/manual.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.createHighlightCallback = exports.HTMLCodeBlockElement = void 0; 7 | const HTMLCodeBlockElement_1 = __importDefault(require("./class/HTMLCodeBlockElement")); 8 | const createHighlightCallback_1 = require("./utils/createHighlightCallback"); 9 | exports.HTMLCodeBlockElement = HTMLCodeBlockElement_1.default; 10 | exports.createHighlightCallback = createHighlightCallback_1.createHighlightCallback; 11 | -------------------------------------------------------------------------------- /dist/stylesheet.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: "\n code-block {\n position: relative;\n margin: 1em 0;\n display: block;\n font-size: 80%;\n font-family: Consolas, Monaco, monospace;\n }\n code-block span[slot=\"name\"] {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 0;\n padding: 0 5px;\n max-width: 90%;\n color: #fff;\n white-space: pre;\n line-height: 1.5;\n overflow: hidden;\n text-overflow: ellipsis;\n background: #75758a;\n box-sizing: border-box;\n }\n code-block button {\n all: unset;\n outline: revert;\n position: absolute;\n right: 0;\n top: 0;\n z-index: 1;\n padding: 10px;\n display: block;\n font-family: inherit;\n color: #fff;\n opacity: 0;\n mix-blend-mode: exclusion;\n }\n\n code-block:hover button,\n code-block button:focus {\n opacity: 1;\n }\n\n code-block pre,\n code-block code {\n font-family: inherit;\n }\n code-block pre {\n margin: 0;\n }\n code-block code {\n padding: 1em;\n display: block;\n font-size: 100%;\n overflow-x: auto;\n }\n code-block[label]:not([label=\"\"]) pre code {\n padding-top: 2em;\n }\n "; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /dist/stylesheet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.default = ` 4 | code-block { 5 | position: relative; 6 | margin: 1em 0; 7 | display: block; 8 | font-size: 80%; 9 | font-family: Consolas, Monaco, monospace; 10 | } 11 | code-block span[slot="name"] { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | z-index: 0; 16 | padding: 0 5px; 17 | max-width: 90%; 18 | color: #fff; 19 | white-space: pre; 20 | line-height: 1.5; 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | background: #75758a; 24 | box-sizing: border-box; 25 | } 26 | code-block button { 27 | all: unset; 28 | outline: revert; 29 | position: absolute; 30 | right: 0; 31 | top: 0; 32 | z-index: 1; 33 | padding: 10px; 34 | display: block; 35 | font-family: inherit; 36 | color: #fff; 37 | opacity: 0; 38 | mix-blend-mode: exclusion; 39 | } 40 | 41 | code-block:hover button, 42 | code-block button:focus { 43 | opacity: 1; 44 | } 45 | 46 | code-block pre, 47 | code-block code { 48 | font-family: inherit; 49 | } 50 | code-block pre { 51 | margin: 0; 52 | } 53 | code-block code { 54 | padding: 1em; 55 | display: block; 56 | font-size: 100%; 57 | overflow-x: auto; 58 | } 59 | code-block[label]:not([label=""]) pre code { 60 | padding-top: 2em; 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /dist/utils/createHighlightCallback.d.ts: -------------------------------------------------------------------------------- 1 | import { HLJSApi } from 'highlight.js'; 2 | import { EndgineFunction } from '../class/HTMLCodeBlockElement'; 3 | /** 4 | * Callback maker for highlight.js 5 | * @param endgine - A library for performing syntax highlighting. 6 | * @return - A function for HTMLCodeBlockElement.highlight 7 | */ 8 | export declare const createHighlightCallback: (endgine: HLJSApi) => EndgineFunction; 9 | -------------------------------------------------------------------------------- /dist/utils/createHighlightCallback.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.createHighlightCallback = void 0; 4 | /** 5 | * Callback maker for highlight.js 6 | * @param endgine - A library for performing syntax highlighting. 7 | * @return - A function for HTMLCodeBlockElement.highlight 8 | */ 9 | const createHighlightCallback = (endgine) => ({ src, options }) => { 10 | const hljs = endgine; 11 | if ( 12 | // Verifying the existence of a language 13 | options?.language && 14 | hljs.getLanguage(options.language)) { 15 | return { 16 | markup: hljs.highlight(src, options).value, 17 | }; 18 | } 19 | return { 20 | markup: hljs.highlightAuto(src).value, 21 | }; 22 | }; 23 | exports.createHighlightCallback = createHighlightCallback; 24 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/qn/p8by0f3j2438n8mdrr1f7gj00000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: 'coverage', 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: 'v8', 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'd.ts', 'tsx'], 75 | 76 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 77 | // moduleNameMapper: {}, 78 | 79 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 80 | // modulePathIgnorePatterns: [], 81 | 82 | // Activates notifications for test results 83 | // notify: false, 84 | 85 | // An enum that specifies notification mode. Requires { notify: true } 86 | // notifyMode: "failure-change", 87 | 88 | // A preset that is used as a base for Jest's configuration 89 | // preset: undefined, 90 | 91 | // Run tests from one or more projects 92 | // projects: undefined, 93 | 94 | // Use this configuration option to add custom reporters to Jest 95 | // reporters: undefined, 96 | 97 | // Automatically reset mock state between every test 98 | // resetMocks: false, 99 | 100 | // Reset the module registry before running each individual test 101 | // resetModules: false, 102 | 103 | // A path to a custom resolver 104 | // resolver: undefined, 105 | 106 | // Automatically restore mock state between every test 107 | // restoreMocks: false, 108 | 109 | // The root directory that Jest should scan for tests and modules within 110 | // rootDir: undefined, 111 | 112 | // A list of paths to directories that Jest should use to search for files in 113 | // roots: [ 114 | // "" 115 | // ], 116 | 117 | // Allows you to use a custom runner instead of Jest's default test runner 118 | // runner: "jest-runner", 119 | 120 | // The paths to modules that run some code to configure or set up the testing environment before each test 121 | // setupFiles: [], 122 | 123 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 124 | // setupFilesAfterEnv: [], 125 | 126 | // The number of seconds after which a test is considered as slow and reported as such in the results. 127 | // slowTestThreshold: 5, 128 | 129 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 130 | // snapshotSerializers: [], 131 | 132 | // The test environment that will be used for testing 133 | testEnvironment: 'jest-environment-jsdom-global', 134 | 135 | // Options that will be passed to the testEnvironment 136 | // testEnvironmentOptions: {}, 137 | 138 | // Adds a location field to test results 139 | // testLocationInResults: false, 140 | 141 | // The glob patterns Jest uses to detect test files 142 | // testMatch: [ 143 | // "**/__tests__/**/*.[jt]s?(x)", 144 | // "**/?(*.)+(spec|test).[tj]s?(x)" 145 | // ], 146 | 147 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 148 | // testPathIgnorePatterns: [ 149 | // "/node_modules/" 150 | // ], 151 | 152 | // The regexp pattern or array of patterns that Jest uses to detect test files 153 | // testRegex: [], 154 | 155 | // This option allows the use of a custom results processor 156 | // testResultsProcessor: undefined, 157 | 158 | // This option allows use of a custom test runner 159 | // testRunner: "jasmine2", 160 | 161 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 162 | // testURL: "http://localhost", 163 | 164 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 165 | // timers: "real", 166 | 167 | // A map from regular expressions to paths to transformers 168 | transform: { 169 | '^.+\\.tsx?$': 'ts-jest', 170 | }, 171 | 172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 173 | // transformIgnorePatterns: [ 174 | // "/node_modules/", 175 | // "\\.pnp\\.[^\\/]+$" 176 | // ], 177 | 178 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 179 | // unmockedModulePathPatterns: undefined, 180 | 181 | // Indicates whether each individual test should be reported during the run 182 | verbose: true, 183 | 184 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 185 | // watchPathIgnorePatterns: [], 186 | 187 | // Whether to use watchman for file crawling 188 | // watchman: true, 189 | }; 190 | -------------------------------------------------------------------------------- /lib/html-code-block-element.all.min.licenses.txt: -------------------------------------------------------------------------------- 1 | highlight.js 2 | BSD-3-Clause 3 | BSD 3-Clause License 4 | 5 | Copyright (c) 2006, Ivan Sagalaev. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /lib/html-code-block-element.common.min.licenses.txt: -------------------------------------------------------------------------------- 1 | highlight.js 2 | BSD-3-Clause 3 | BSD 3-Clause License 4 | 5 | Copyright (c) 2006, Ivan Sagalaev. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /lib/html-code-block-element.core.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @heppokofrontend/html-code-block-element v2.0.5 3 | * author: heppokofrontend 4 | * license: MIT 5 | */!function(){var e={9646:function(e,t){"use strict";function n(e,t,n){i(e,t),t.set(e,n)}function i(e,t){if(t.has(e))throw new TypeError("Cannot initialize the same private elements twice on an object")}function o(e,t,n){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return n}function r(e,t,n){return l(e,a(e,t,"set"),n),n}function s(e,t){return h(e,a(e,t,"get"))}function a(e,t,n){if(!t.has(e))throw new TypeError("attempted to "+n+" private field on non-instance");return t.get(e)}function l(e,t,n){if(t.set)t.set.call(e,n);else{if(!t.writable)throw new TypeError("attempted to set read only private field");t.value=n}}function c(e,t,n){return d(e,t),u(n,"get"),h(e,n)}function u(e,t){if(void 0===e)throw new TypeError("attempted to "+t+" private static field before its declaration")}function d(e,t){if(e!==t)throw new TypeError("Private static access of wrong provenance")}function h(e,t){return t.get?t.get.call(e):t.value}Object.defineProperty(t,"__esModule",{value:!0});var g=new WeakMap,f=new WeakMap,p=new WeakMap,b=new WeakMap,m=new WeakMap,w=new WeakMap,E=new WeakMap,x=new WeakMap,v=new WeakMap,y=new WeakMap,k=new WeakMap,M=new WeakMap,_=new WeakMap,T=new WeakSet;class HTMLCodeBlockElement extends HTMLElement{static get highlight(){const e=c(HTMLCodeBlockElement,HTMLCodeBlockElement,C);if(e===c(HTMLCodeBlockElement,HTMLCodeBlockElement,A))throw new TypeError("The syntax highlighting engine is not set to `HTMLCodeBlockElement.highlight`.");return e}static set highlight(e){var t,n,i;n=C,i=e,d(t=HTMLCodeBlockElement,HTMLCodeBlockElement),u(n,"set"),l(t,n,i)}get value(){return s(this,x)}set value(e){r(this,x,String(e)),o(this,T,L).call(this)}get label(){return s(this,v)}set label(e){s(this,v)===e||""===s(this,v)&&null===this.getAttribute("label")&&null===e||(null===e?(r(this,v,""),this.removeAttribute("label")):(r(this,v,String(e)),this.setAttribute("label",s(this,v))),o(this,T,L).call(this))}get language(){return s(this,y)}set language(e){s(this,y)===e||""===s(this,y)&&null===this.getAttribute("language")&&null===e||(null===e?(r(this,y,""),this.removeAttribute("language")):(r(this,y,String(e)),this.setAttribute("language",s(this,y))),o(this,T,L).call(this))}get controls(){return s(this,k)}set controls(e){s(this,k)!==e&&(r(this,k,e),s(this,k)?this.setAttribute("controls",""):this.removeAttribute("controls"),o(this,T,L).call(this))}set notrim(e){s(this,M)!==e&&(r(this,M,e),s(this,M)?this.setAttribute("notrim",""):this.removeAttribute("notrim"),o(this,T,L).call(this))}static get observedAttributes(){return["label","language","controls","notrim"]}attributeChangedCallback(e,t,n){if(t!==n)switch(e){case"label":case"language":this[e]=n;break;case"controls":case"notrim":this[e]="string"==typeof n}}connectedCallback(){!1===s(this,p)&&""===s(this,x)&&r(this,x,this.textContent||""),r(this,p,!0),o(this,T,L).call(this)}constructor(){var e,t;super(),i(e=this,t=T),t.add(e),n(this,g,{writable:!0,value:new MutationObserver((()=>{s(this,g).disconnect();const e=this.querySelectorAll('[slot]:not([slot="code"])');for(const t of e)t.remove();r(this,x,(this.textContent||this.getAttribute("value")||"").replace(/^\n/,"").replace(/\n$/,"")),o(this,T,L).call(this)}))}),n(this,f,{writable:!0,value:{name:c(HTMLCodeBlockElement,HTMLCodeBlockElement,O).call(HTMLCodeBlockElement,{name:"name",id:"name"}),copyButton:c(HTMLCodeBlockElement,HTMLCodeBlockElement,O).call(HTMLCodeBlockElement,{name:"copy-button"}),code:c(HTMLCodeBlockElement,HTMLCodeBlockElement,O).call(HTMLCodeBlockElement,{name:"code"})}}),n(this,p,{writable:!0,value:!1}),n(this,b,{writable:!0,value:void 0}),n(this,m,{writable:!0,value:void 0}),n(this,w,{writable:!0,value:void 0}),n(this,E,{writable:!0,value:void 0}),n(this,x,{writable:!0,value:""}),n(this,v,{writable:!0,value:""}),n(this,y,{writable:!0,value:""}),n(this,k,{writable:!0,value:!1}),n(this,M,{writable:!0,value:!1}),n(this,_,{writable:!0,value:(()=>{let e=-1;return async()=>{const t=s(this,x).replace(/\n$/,"");clearTimeout(e),await(e=>navigator.clipboard?navigator.clipboard.writeText(e):new Promise((t=>{const n=document.createElement("textarea");n.value=e,document.body.append(n),n.select(),document.execCommand("copy"),n.remove(),t()})))(t),s(this,m).classList.add("--copied"),s(this,m).textContent="Copied!",e=window.setTimeout((()=>{s(this,m).classList.remove("--copied"),s(this,m).textContent="Copy"}),1500)}})()});const a=(()=>{const e=document.createElement("span");return e.slot="name",e.textContent=this.getAttribute("label")||"",e})(),l=(()=>{const e=document.createElement("button");return e.type="button",e.slot="copy-button",e.textContent="Copy",e.setAttribute("aria-live","polite"),e.addEventListener("click",s(this,_)),e})(),u=(()=>{const e=document.createElement("code");return e.tabIndex=0,e.className="hljs",e})(),d=(()=>{const e=document.createElement("pre");return e.slot="code",e.append(u),e})(),h=(()=>{const e=document.createElement("span");return e.id="semantics",e.hidden=!0,e.textContent="Code Block",e})(),A=(()=>{const e=document.createElement("div");return e.append(...Object.values(s(this,f))),e.setAttribute("role","group"),e.setAttribute("aria-labelledby","semantics name"),e})(),C=this.attachShadow({mode:"closed"});C.append(h),C.append(A),r(this,x,(this.textContent||"").replace(/^\n/,"").replace(/\n$/,"")),r(this,v,a.textContent||""),r(this,y,this.getAttribute("language")||""),r(this,k,null!==this.getAttribute("controls")),r(this,M,null!==this.getAttribute("notrim")),r(this,b,a),r(this,m,l),r(this,w,u),r(this,E,d)}}function L(){if(!this.parentNode)return;s(this,g).disconnect();const e=s(this,M)?s(this,x):s(this,x).trim(),{markup:t}=HTMLCodeBlockElement.highlight({src:e,options:{language:s(this,y)}});this.textContent="",s(this,b).textContent=s(this,v),s(this,f).name.hidden=!s(this,v),s(this,f).copyButton.hidden=!s(this,k),s(this,w).textContent="",s(this,w).insertAdjacentHTML("afterbegin",t),this.append(s(this,b)),this.append(s(this,m)),this.append(s(this,E)),s(this,g).observe(this,{childList:!0})}var A={writable:!0,value:e=>{let{src:t}=e;return{markup:t}}},C={writable:!0,value:c(HTMLCodeBlockElement,HTMLCodeBlockElement,A)},O={writable:!0,value:e=>{let{name:t,id:n=""}=e;const i=document.createElement("slot");return i.name=t,n&&(i.id=n),i}};t.default=HTMLCodeBlockElement,Object.defineProperty(HTMLCodeBlockElement,"name",{value:"HTMLCodeBlockElement"})},5471:function(e,t,n){"use strict";var i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const o=i(n(9927)),r=document.createElement("style"),s=document.querySelector("head link, head style");r.textContent=o.default,r.dataset.id="html-code-block-element",s?s.before(r):document.head.append(r)},8099:function(e,t,n){"use strict";var i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.HTMLCodeBlockElement=void 0;const o=i(n(3384)),r=i(n(9646)),s=n(2899);n(5471),r.default.highlight=(0,s.createHighlightCallback)(o.default),customElements.define("code-block",r.default),t.HTMLCodeBlockElement=r.default},9927:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default='\n code-block {\n position: relative;\n margin: 1em 0;\n display: block;\n font-size: 80%;\n font-family: Consolas, Monaco, monospace;\n }\n code-block span[slot="name"] {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 0;\n padding: 0 5px;\n max-width: 90%;\n color: #fff;\n white-space: pre;\n line-height: 1.5;\n overflow: hidden;\n text-overflow: ellipsis;\n background: #75758a;\n box-sizing: border-box;\n }\n code-block button {\n all: unset;\n outline: revert;\n position: absolute;\n right: 0;\n top: 0;\n z-index: 1;\n padding: 10px;\n display: block;\n font-family: inherit;\n color: #fff;\n opacity: 0;\n mix-blend-mode: exclusion;\n }\n\n code-block:hover button,\n code-block button:focus {\n opacity: 1;\n }\n\n code-block pre,\n code-block code {\n font-family: inherit;\n }\n code-block pre {\n margin: 0;\n }\n code-block code {\n padding: 1em;\n display: block;\n font-size: 100%;\n overflow-x: auto;\n }\n code-block[label]:not([label=""]) pre code {\n padding-top: 2em;\n }\n '},2899:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createHighlightCallback=void 0;t.createHighlightCallback=e=>t=>{let{src:n,options:i}=t;const o=e;return null!=i&&i.language&&o.getLanguage(i.language)?{markup:o.highlight(n,i).value}:{markup:o.highlightAuto(n).value}}},3384:function(e){var t={exports:{}};function n(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((function(t){var i=e[t];"object"!=typeof i||Object.isFrozen(i)||n(i)})),e}t.exports=n,t.exports.default=n;class Response{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function i(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function o(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t];return t.forEach((function(e){for(const t in e)n[t]=e[t]})),n}const r=e=>!!e.scope||e.sublanguage&&e.language;class HTMLRenderer{constructor(e,t){this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){this.buffer+=i(e)}openNode(e){if(!r(e))return;let t="";t=e.sublanguage?`language-${e.language}`:((e,{prefix:t})=>{if(e.includes(".")){const n=e.split(".");return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ")}return`${t}${e}`})(e.scope,{prefix:this.classPrefix}),this.span(t)}closeNode(e){r(e)&&(this.buffer+="")}value(){return this.buffer}span(e){this.buffer+=``}}const s=(e={})=>{const t={children:[]};return Object.assign(t,e),t};class TokenTree{constructor(){this.rootNode=s(),this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const t=s({scope:e});this.add(t),this.stack.push(t)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{TokenTree._collapse(e)})))}}class TokenTreeEmitter extends TokenTree{constructor(e){super(),this.options=e}addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root;n.sublanguage=!0,n.language=t,this.add(n)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){return!0}}function a(e){return e?"string"==typeof e?e:e.source:null}function l(e){return d("(?=",e,")")}function c(e){return d("(?:",e,")*")}function u(e){return d("(?:",e,")?")}function d(...e){return e.map((e=>a(e))).join("")}function h(...e){const t=function(e){const t=e[e.length-1];return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}}(e);return"("+(t.capture?"":"?:")+e.map((e=>a(e))).join("|")+")"}function g(e){return new RegExp(e.toString()+"|").exec("").length-1}const f=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;function p(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n;let i=a(e),o="";for(;i.length>0;){const e=f.exec(i);if(!e){o+=i;break}o+=i.substring(0,e.index),i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?o+="\\"+String(Number(e[1])+t):(o+=e[0],"("===e[0]&&n++)}return o})).map((e=>`(${e})`)).join(t)}const b="[a-zA-Z]\\w*",m="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",E="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",x="\\b(0b[01]+)",v={begin:"\\\\[\\s\\S]",relevance:0},y={scope:"string",begin:"'",end:"'",illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n",contains:[v]},M=function(e,t,n={}){const i=o({scope:"comment",begin:e,end:t,contains:[]},n);i.contains.push({scope:"doctag",begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0});const r=h("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/);return i.contains.push({begin:d(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i},_=M("//","$"),T=M("/\\*","\\*/"),L=M("#","$"),A={scope:"number",begin:w,relevance:0},C={scope:"number",begin:E,relevance:0},O={scope:"number",begin:x,relevance:0},R={begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}]},S={scope:"title",begin:b,relevance:0},B={scope:"title",begin:m,relevance:0},N={begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0};var j=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:b,UNDERSCORE_IDENT_RE:m,NUMBER_RE:w,C_NUMBER_RE:E,BINARY_NUMBER_RE:x,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const t=/^#![ ]*\//;return e.binary&&(e.begin=d(t,/.*\b/,e.binary,/\b.*/)),o({scope:"meta",begin:t,end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},BACKSLASH_ESCAPE:v,APOS_STRING_MODE:y,QUOTE_STRING_MODE:k,PHRASAL_WORDS_MODE:{begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT:M,C_LINE_COMMENT_MODE:_,C_BLOCK_COMMENT_MODE:T,HASH_COMMENT_MODE:L,NUMBER_MODE:A,C_NUMBER_MODE:C,BINARY_NUMBER_MODE:O,REGEXP_MODE:R,TITLE_MODE:S,UNDERSCORE_TITLE_MODE:B,METHOD_GUARD:N,END_SAME_AS_BEGIN:function(e){return Object.assign(e,{"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{t.data._beginMatch!==e[1]&&t.ignoreMatch()}})}});function H(e,t){"."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){void 0!==e.className&&(e.scope=e.className,delete e.className)}function P(e,t){t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",e.__beforeBegin=H,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,void 0===e.relevance&&(e.relevance=0))}function D(e,t){Array.isArray(e.illegal)&&(e.illegal=h(...e.illegal))}function $(e,t){if(e.match){if(e.begin||e.end)throw new Error("begin & end are not supported with match");e.begin=e.match,delete e.match}}function W(e,t){void 0===e.relevance&&(e.relevance=1)}const z=(e,t)=>{if(!e.beforeMatch)return;if(e.starts)throw new Error("beforeMatch cannot be used with starts");const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t]})),e.keywords=n.keywords,e.begin=d(n.beforeMatch,l(n.begin)),e.starts={relevance:0,contains:[Object.assign(n,{endsParent:!0})]},e.relevance=0,delete n.beforeMatch},U=["of","and","for","in","not","or","if","then","parent","list","value"];function K(e,t,n="keyword"){const i=Object.create(null);return"string"==typeof e?o(n,e.split(" ")):Array.isArray(e)?o(n,e):Object.keys(e).forEach((function(n){Object.assign(i,K(e[n],t,n))})),i;function o(e,n){t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((function(t){const n=t.split("|");i[n[0]]=[e,X(n[0],n[1])]}))}}function X(e,t){return t?Number(t):function(e){return U.includes(e.toLowerCase())}(e)?0:1}const G={},Z=e=>{console.error(e)},F=(e,...t)=>{console.log(`WARN: ${e}`,...t)},q=(e,t)=>{G[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),G[`${e}/${t}`]=!0)},V=new Error;function J(e,t,{key:n}){let i=0;const o=e[n],r={},s={};for(let e=1;e<=t.length;e++)s[e+i]=o[e],r[e+i]=!0,i+=g(t[e-1]);e[n]=s,e[n]._emit=r,e[n]._multi=!0}function Y(e){!function(e){e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,delete e.scope)}(e),"string"==typeof e.beginScope&&(e.beginScope={_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope}),function(e){if(Array.isArray(e.begin)){if(e.skip||e.excludeBegin||e.returnBegin)throw Z("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),V;if("object"!=typeof e.beginScope||null===e.beginScope)throw Z("beginScope must be object"),V;J(e,e.begin,{key:"beginScope"}),e.begin=p(e.begin,{joinWith:""})}}(e),function(e){if(Array.isArray(e.end)){if(e.skip||e.excludeEnd||e.returnEnd)throw Z("skip, excludeEnd, returnEnd not compatible with endScope: {}"),V;if("object"!=typeof e.endScope||null===e.endScope)throw Z("endScope must be object"),V;J(e,e.end,{key:"endScope"}),e.end=p(e.end,{joinWith:""})}}(e)}function Q(e){function t(t,n){return new RegExp(a(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":""))}class MultiRegex{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,t){t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),this.matchAt+=g(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map((e=>e[1]));this.matcherRe=t(p(e,{joinWith:"|"}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const t=this.matcherRe.exec(e);if(!t)return null;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n];return t.splice(0,n),Object.assign(t,i)}}class ResumableMultiRegex{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const t=new MultiRegex;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex;let n=t.exec(e);if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}return n&&(this.regexIndex+=n.position+1,this.regexIndex===this.count&&this.considerAll()),n}}if(e.compilerExtensions||(e.compilerExtensions=[]),e.contains&&e.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=o(e.classNameAliases||{}),function n(i,r){const s=i;if(i.isCompiled)return s;[I,$,Y,z].forEach((e=>e(i,r))),e.compilerExtensions.forEach((e=>e(i,r))),i.__beforeBegin=null,[P,D,W].forEach((e=>e(i,r))),i.isCompiled=!0;let l=null;return"object"==typeof i.keywords&&i.keywords.$pattern&&(i.keywords=Object.assign({},i.keywords),l=i.keywords.$pattern,delete i.keywords.$pattern),l=l||/\w+/,i.keywords&&(i.keywords=K(i.keywords,e.case_insensitive)),s.keywordPatternRe=t(l,!0),r&&(i.begin||(i.begin=/\B|\b/),s.beginRe=t(s.begin),i.end||i.endsWithParent||(i.end=/\B|\b/),i.end&&(s.endRe=t(s.end)),s.terminatorEnd=a(s.end)||"",i.endsWithParent&&r.terminatorEnd&&(s.terminatorEnd+=(i.end?"|":"")+r.terminatorEnd)),i.illegal&&(s.illegalRe=t(i.illegal)),i.contains||(i.contains=[]),i.contains=[].concat(...i.contains.map((function(e){return function(e){e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((function(t){return o(e,{variants:null},t)})));if(e.cachedVariants)return e.cachedVariants;if(ee(e))return o(e,{starts:e.starts?o(e.starts):null});if(Object.isFrozen(e))return o(e);return e}("self"===e?i:e)}))),i.contains.forEach((function(e){n(e,s)})),i.starts&&n(i.starts,r),s.matcher=function(e){const t=new ResumableMultiRegex;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t}(s),s}(e)}function ee(e){return!!e&&(e.endsWithParent||ee(e.starts))}class HTMLInjectionError extends Error{constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}}const te=i,ne=o,ie=Symbol("nomatch");var oe=function(e){const n=Object.create(null),i=Object.create(null),o=[];let r=!0;const s="Could not find the language '{}', did you forget to load/include a language module?",a={disableAutodetect:!0,name:"Plain text",contains:[]};let g={ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",cssSelector:"pre code",languages:null,__emitter:TokenTreeEmitter};function f(e){return g.noHighlightRe.test(e)}function p(e,t,n){let i="",o="";"object"==typeof t?(i=e,n=t.ignoreIllegals,o=t.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."),q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),o=e,i=t),void 0===n&&(n=!0);const r={code:i,language:o};M("before:highlight",r);const s=r.result?r.result:b(r.language,r.code,n);return s.code=r.code,M("after:highlight",s),s}function b(e,t,i,o){const a=Object.create(null);function l(){if(!M.keywords)return void T.addText(L);let e=0;M.keywordPatternRe.lastIndex=0;let t=M.keywordPatternRe.exec(L),n="";for(;t;){n+=L.substring(e,t.index);const o=x.case_insensitive?t[0].toLowerCase():t[0],r=(i=o,M.keywords[i]);if(r){const[e,i]=r;if(T.addText(n),n="",a[o]=(a[o]||0)+1,a[o]<=7&&(A+=i),e.startsWith("_"))n+=t[0];else{const n=x.classNameAliases[e]||e;T.addKeyword(t[0],n)}}else n+=t[0];e=M.keywordPatternRe.lastIndex,t=M.keywordPatternRe.exec(L)}var i;n+=L.substring(e),T.addText(n)}function c(){null!=M.subLanguage?function(){if(""===L)return;let e=null;if("string"==typeof M.subLanguage){if(!n[M.subLanguage])return void T.addText(L);e=b(M.subLanguage,L,!0,_[M.subLanguage]),_[M.subLanguage]=e._top}else e=m(L,M.subLanguage.length?M.subLanguage:null);M.relevance>0&&(A+=e.relevance),T.addSublanguage(e._emitter,e.language)}():l(),L=""}function u(e,t){let n=1;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue}const i=x.classNameAliases[e[n]]||e[n],o=t[n];i?T.addKeyword(o,i):(L=o,l(),L=""),n++}}function d(e,t){return e.scope&&"string"==typeof e.scope&&T.openNode(x.classNameAliases[e.scope]||e.scope),e.beginScope&&(e.beginScope._wrap?(T.addKeyword(L,x.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),L=""):e.beginScope._multi&&(u(e.beginScope,t),L="")),M=Object.create(e,{parent:{value:M}}),M}function h(e,t,n){let i=function(e,t){const n=e&&e.exec(t);return n&&0===n.index}(e.endRe,n);if(i){if(e["on:end"]){const n=new Response(e);e["on:end"](t,n),n.isMatchIgnored&&(i=!1)}if(i){for(;e.endsParent&&e.parent;)e=e.parent;return e}}if(e.endsWithParent)return h(e.parent,t,n)}function f(e){return 0===M.matcher.regexIndex?(L+=e[0],1):(R=!0,0)}function p(e){const n=e[0],i=t.substring(e.index),o=h(M,e,i);if(!o)return ie;const r=M;M.endScope&&M.endScope._wrap?(c(),T.addKeyword(n,M.endScope._wrap)):M.endScope&&M.endScope._multi?(c(),u(M.endScope,e)):r.skip?L+=n:(r.returnEnd||r.excludeEnd||(L+=n),c(),r.excludeEnd&&(L=n));do{M.scope&&T.closeNode(),M.skip||M.subLanguage||(A+=M.relevance),M=M.parent}while(M!==o.parent);return o.starts&&d(o.starts,e),r.returnEnd?0:n.length}let w={};function E(n,o){const s=o&&o[0];if(L+=n,null==s)return c(),0;if("begin"===w.type&&"end"===o.type&&w.index===o.index&&""===s){if(L+=t.slice(o.index,o.index+1),!r){const t=new Error(`0 width match regex (${e})`);throw t.languageName=e,t.badRule=w.rule,t}return 1}if(w=o,"begin"===o.type)return function(e){const t=e[0],n=e.rule,i=new Response(n),o=[n.__beforeBegin,n["on:begin"]];for(const n of o)if(n&&(n(e,i),i.isMatchIgnored))return f(t);return n.skip?L+=t:(n.excludeBegin&&(L+=t),c(),n.returnBegin||n.excludeBegin||(L=t)),d(n,e),n.returnBegin?0:t.length}(o);if("illegal"===o.type&&!i){const e=new Error('Illegal lexeme "'+s+'" for mode "'+(M.scope||"")+'"');throw e.mode=M,e}if("end"===o.type){const e=p(o);if(e!==ie)return e}if("illegal"===o.type&&""===s)return 1;if(O>1e5&&O>3*o.index){throw new Error("potential infinite loop, way more iterations than matches")}return L+=s,s.length}const x=v(e);if(!x)throw Z(s.replace("{}",e)),new Error('Unknown language: "'+e+'"');const y=Q(x);let k="",M=o||y;const _={},T=new g.__emitter(g);!function(){const e=[];for(let t=M;t!==x;t=t.parent)t.scope&&e.unshift(t.scope);e.forEach((e=>T.openNode(e)))}();let L="",A=0,C=0,O=0,R=!1;try{for(M.matcher.considerAll();;){O++,R?R=!1:M.matcher.considerAll(),M.matcher.lastIndex=C;const e=M.matcher.exec(t);if(!e)break;const n=E(t.substring(C,e.index),e);C=e.index+n}return E(t.substring(C)),T.closeAllNodes(),T.finalize(),k=T.toHTML(),{language:e,value:k,relevance:A,illegal:!1,_emitter:T,_top:M}}catch(n){if(n.message&&n.message.includes("Illegal"))return{language:e,value:te(t),illegal:!0,relevance:0,_illegalBy:{message:n.message,index:C,context:t.slice(C-100,C+100),mode:n.mode,resultSoFar:k},_emitter:T};if(r)return{language:e,value:te(t),illegal:!1,relevance:0,errorRaised:n,_emitter:T,_top:M};throw n}}function m(e,t){t=t||g.languages||Object.keys(n);const i=function(e){const t={value:te(e),illegal:!1,relevance:0,_top:a,_emitter:new g.__emitter(g)};return t._emitter.addText(e),t}(e),o=t.filter(v).filter(k).map((t=>b(t,e,!1)));o.unshift(i);const r=o.sort(((e,t)=>{if(e.relevance!==t.relevance)return t.relevance-e.relevance;if(e.language&&t.language){if(v(e.language).supersetOf===t.language)return 1;if(v(t.language).supersetOf===e.language)return-1}return 0})),[s,l]=r,c=s;return c.secondBest=l,c}function w(e){let t=null;const n=function(e){let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"";const n=g.languageDetectRe.exec(t);if(n){const t=v(n[1]);return t||(F(s.replace("{}",n[1])),F("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}return t.split(/\s+/).find((e=>f(e)||v(e)))}(e);if(f(n))return;if(M("before:highlightElement",{el:e,language:n}),e.children.length>0&&(g.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),console.warn("The element with unescaped HTML:"),console.warn(e)),g.throwUnescapedHTML)){throw new HTMLInjectionError("One of your code blocks includes unescaped HTML.",e.innerHTML)}t=e;const o=t.textContent,r=n?p(o,{language:n,ignoreIllegals:!0}):m(o);e.innerHTML=r.value,function(e,t,n){const o=t&&i[t]||n;e.classList.add("hljs"),e.classList.add(`language-${o}`)}(e,n,r.language),e.result={language:r.language,re:r.relevance,relevance:r.relevance},r.secondBest&&(e.secondBest={language:r.secondBest.language,relevance:r.secondBest.relevance}),M("after:highlightElement",{el:e,result:r,text:o})}let E=!1;function x(){if("loading"===document.readyState)return void(E=!0);document.querySelectorAll(g.cssSelector).forEach(w)}function v(e){return e=(e||"").toLowerCase(),n[e]||n[i[e]]}function y(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{i[e.toLowerCase()]=t}))}function k(e){const t=v(e);return t&&!t.disableAutodetect}function M(e,t){const n=e;o.forEach((function(e){e[n]&&e[n](t)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function(){E&&x()}),!1),Object.assign(e,{highlight:p,highlightAuto:m,highlightAll:x,highlightElement:w,highlightBlock:function(e){return q("10.7.0","highlightBlock will be removed entirely in v12.0"),q("10.7.0","Please use highlightElement now."),w(e)},configure:function(e){g=ne(g,e)},initHighlighting:()=>{x(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},initHighlightingOnLoad:function(){x(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")},registerLanguage:function(t,i){let o=null;try{o=i(e)}catch(e){if(Z("Language definition for '{}' could not be registered.".replace("{}",t)),!r)throw e;Z(e),o=a}o.name||(o.name=t),n[t]=o,o.rawDefinition=i.bind(null,e),o.aliases&&y(o.aliases,{languageName:t})},unregisterLanguage:function(e){delete n[e];for(const t of Object.keys(i))i[t]===e&&delete i[t]},listLanguages:function(){return Object.keys(n)},getLanguage:v,registerAliases:y,autoDetection:k,inherit:ne,addPlugin:function(e){!function(e){e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{e["before:highlightBlock"](Object.assign({block:t.el},t))}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{e["after:highlightBlock"](Object.assign({block:t.el},t))})}(e),o.push(e)}}),e.debugMode=function(){r=!1},e.safeMode=function(){r=!0},e.versionString="11.6.0",e.regex={concat:d,lookahead:l,either:h,optional:u,anyNumberOfTimes:c};for(const e in j)"object"==typeof j[e]&&t.exports(j[e]);return Object.assign(e,j),e}({});e.exports=oe,oe.HighlightJS=oe,oe.default=oe}},t={};var n=function n(i){var o=t[i];if(void 0!==o)return o.exports;var r=t[i]={exports:{}};return e[i].call(r.exports,r,r.exports,n),r.exports}(8099);window.HTMLCodeBlockElement=n.HTMLCodeBlockElement}(); -------------------------------------------------------------------------------- /lib/html-code-block-element.core.min.licenses.txt: -------------------------------------------------------------------------------- 1 | highlight.js 2 | BSD-3-Clause 3 | BSD 3-Clause License 4 | 5 | Copyright (c) 2006, Ivan Sagalaev. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heppokofrontend/html-code-block-element", 3 | "description": "Code block custom element with syntax highlighting and copy button.", 4 | "version": "2.0.5", 5 | "author": "heppokofrontend", 6 | "bugs": { 7 | "url": "https://github.com/heppokofrontend/html-code-block-element/issues" 8 | }, 9 | "dependencies": { 10 | "highlight.js": "^11.1.0" 11 | }, 12 | "devDependencies": { 13 | "@babel/cli": "^7.14.8", 14 | "@babel/core": "^7.14.8", 15 | "@babel/preset-env": "^7.14.8", 16 | "@babel/preset-typescript": "^7.14.5", 17 | "@types/jest": "^26.0.20", 18 | "@types/node": "^14.14.35", 19 | "@types/react": "^17.0.20", 20 | "@types/react-test-renderer": "^17.0.2", 21 | "@typescript-eslint/eslint-plugin": "^4.33.0", 22 | "@typescript-eslint/parser": "^4.28.5", 23 | "babel-loader": "^8.2.2", 24 | "eslint": "^7.32.0", 25 | "eslint-config-google": "^0.14.0", 26 | "eslint-config-prettier": "^8.5.0", 27 | "gh-pages": "^3.2.3", 28 | "jest": "^26.6.3", 29 | "jest-environment-jsdom-global": "^2.0.4", 30 | "license-webpack-plugin": "^2.3.20", 31 | "prettier": "^2.7.1", 32 | "react": "^18.2.0", 33 | "react-test-renderer": "^18.2.0", 34 | "run-script-os": "^1.1.6", 35 | "terser-webpack-plugin": "^5.1.4", 36 | "ts-jest": "^26.5.4", 37 | "ts-node": "^9.1.1", 38 | "tsc-alias": "^1.7.0", 39 | "typescript": "^4.7.4", 40 | "webpack": "^5.27.0", 41 | "webpack-cli": "^4.10.0", 42 | "webpack-dev-server": "^4.9.3" 43 | }, 44 | "directories": { 45 | "test": "test" 46 | }, 47 | "files": [ 48 | "dist", 49 | "lib", 50 | "@types" 51 | ], 52 | "homepage": "https://github.com/heppokofrontend/html-code-block-element#readme", 53 | "keywords": [ 54 | "code-block", 55 | "code-blocks", 56 | "custom-elements", 57 | "highlight", 58 | "highlight.js", 59 | "syntax-highlighting", 60 | "web-components" 61 | ], 62 | "license": "MIT", 63 | "main": "dist/index.common.js", 64 | "repository": { 65 | "type": "git", 66 | "url": "git+https://github.com/heppokofrontend/code-block-element.git" 67 | }, 68 | "scripts": { 69 | "build": "npm run tsc:build", 70 | "bundle": "webpack --mode=production", 71 | "clean": "run-script-os", 72 | "clean:default": "rm -rf dist && rm -rf lib", 73 | "clean:win32": "(If exist dist rmdir dist /s /q) && (If exist lib rmdir lib /s /q)", 74 | "deploy:demo": "gh-pages -d demo", 75 | "jest": "jest --coverage --verbose", 76 | "jest:watch": "jest --watch --coverage --verbose", 77 | "lint": "eslint \"./src/**/*.ts\"", 78 | "lint:fix": "npm run lint -- --fix", 79 | "postbuild": "npm run bundle", 80 | "prebuild": "npm run clean", 81 | "prepublishOnly": "npm run build", 82 | "prestart": "git config commit.template .gitmessage", 83 | "prettier": "prettier --write \"./**/*.{json,js,ts}\"", 84 | "start": "webpack serve --mode=production", 85 | "test": "npm run tsc && npm run lint -- --quiet && npm run jest", 86 | "tsc": "tsc --noEmit", 87 | "tsc:build": "tsc && tsc-alias" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/class/HTMLCodeBlockElement.ts: -------------------------------------------------------------------------------- 1 | export type EndgineProps = { 2 | src: string; 3 | options?: { 4 | /** Language Mode Name */ 5 | language: string; 6 | }; 7 | }; 8 | 9 | export type EndgineFunction = (props: EndgineProps) => { 10 | markup: string; 11 | }; 12 | 13 | export default class HTMLCodeBlockElement extends HTMLElement { 14 | static #defaultEndgine: EndgineFunction = ({src}) => ({ 15 | markup: src, 16 | }); 17 | 18 | static #endgine = HTMLCodeBlockElement.#defaultEndgine; 19 | 20 | /** 21 | * Returns the result of highlighting the received source code string. 22 | * Before running `customElements.define()`, 23 | * you need to assign it directly to `HTMLCodeBlockElement.highlight`. 24 | */ 25 | static get highlight(): EndgineFunction { 26 | const endgine = HTMLCodeBlockElement.#endgine; 27 | 28 | if (endgine === HTMLCodeBlockElement.#defaultEndgine) { 29 | throw new TypeError( 30 | 'The syntax highlighting engine is not set to `HTMLCodeBlockElement.highlight`.' 31 | ); 32 | } 33 | 34 | return endgine; 35 | } 36 | 37 | static set highlight(endgine: EndgineFunction) { 38 | HTMLCodeBlockElement.#endgine = endgine; 39 | } 40 | 41 | static #createSlotElement = ({ 42 | name, 43 | id = '', 44 | }: { 45 | name: string; 46 | id?: string; 47 | }): HTMLSlotElement => { 48 | const slot = document.createElement('slot'); 49 | 50 | slot.name = name; 51 | 52 | if (id) { 53 | slot.id = id; 54 | } 55 | 56 | return slot; 57 | }; 58 | 59 | /** Observer to monitor the editing of the content of this element. */ 60 | #observer = new MutationObserver(() => { 61 | this.#observer.disconnect(); 62 | 63 | // Remove elements other than element with `code` as `slot` attribute value. 64 | // The content of the `[slot="code"]` element will be passed to next rendering. 65 | const slots = this.querySelectorAll('[slot]:not([slot="code"])'); 66 | 67 | for (const slot of slots) { 68 | slot.remove(); 69 | } 70 | 71 | this.#value = (this.textContent || this.getAttribute('value') || '') 72 | .replace(/^\n/, '') 73 | .replace(/\n$/, ''); 74 | 75 | this.#render(); 76 | }); 77 | 78 | /** Slot elements for Shadow DOM content */ 79 | #slots = { 80 | name: HTMLCodeBlockElement.#createSlotElement({name: 'name', id: 'name'}), 81 | copyButton: HTMLCodeBlockElement.#createSlotElement({name: 'copy-button'}), 82 | code: HTMLCodeBlockElement.#createSlotElement({name: 'code'}), 83 | }; 84 | 85 | /** 86 | * True when rendered at least once. 87 | * The purpose of this flag is to available the operation the following usage. 88 | * 89 | * Specifically, this is the case where an element is rendered 90 | * on the screen without ever using the value property. 91 | * 92 | * ```js 93 | * const cb = document.createElement('code-block'); 94 | * 95 | * cb.language = 'json'; 96 | * cb.textContent = '{"a": 100}'; 97 | * document.body.prepend(cb); 98 | * ``` 99 | */ 100 | #rendered = false; 101 | 102 | /** Pure DOM content */ 103 | #a11yName: HTMLElement; 104 | 105 | /** Pure DOM content */ 106 | #copyButton: HTMLButtonElement; 107 | 108 | /** Pure DOM content */ 109 | #codeBlock: HTMLElement; 110 | 111 | /** Pure DOM content */ 112 | #codeWrap: HTMLPreElement; 113 | 114 | /** Actual value of the accessor `value` */ 115 | #value = ''; 116 | 117 | /** Actual value of the accessor `label` */ 118 | #label = ''; 119 | 120 | /** Actual value of the accessor `language` */ 121 | #language = ''; 122 | 123 | /** Actual value of the accessor `controls` */ 124 | #controls = false; 125 | 126 | /** Actual value of the accessor `notrim` */ 127 | #notrim = false; 128 | 129 | /** Click event handler of copy button */ 130 | #onClickButton = (() => { 131 | let key = -1; 132 | /** 133 | * Write to the clipboard. 134 | * @param value - String to be saved to the clipboard 135 | * @return - A promise 136 | */ 137 | const copy = (value: string): Promise => { 138 | if (navigator.clipboard) { 139 | return navigator.clipboard.writeText(value); 140 | } 141 | 142 | return new Promise((r) => { 143 | const textarea = document.createElement('textarea'); 144 | 145 | textarea.value = value; 146 | document.body.append(textarea); 147 | textarea.select(); 148 | document.execCommand('copy'); 149 | textarea.remove(); 150 | r(); 151 | }); 152 | }; 153 | 154 | return async () => { 155 | const value = this.#value.replace(/\n$/, ''); 156 | 157 | clearTimeout(key); 158 | 159 | await copy(value); 160 | 161 | this.#copyButton.classList.add('--copied'); 162 | this.#copyButton.textContent = 'Copied!'; 163 | key = window.setTimeout(() => { 164 | this.#copyButton.classList.remove('--copied'); 165 | this.#copyButton.textContent = 'Copy'; 166 | }, 1500); 167 | }; 168 | })(); 169 | 170 | /** Outputs the resulting syntax-highlighted markup to the DOM. */ 171 | #render(): void { 172 | if (!this.parentNode) { 173 | return; 174 | } 175 | 176 | this.#observer.disconnect(); 177 | 178 | const src = this.#notrim ? this.#value : this.#value.trim(); 179 | 180 | /** The resulting syntax-highlighted markup */ 181 | const {markup} = HTMLCodeBlockElement.highlight({ 182 | src, 183 | options: { 184 | language: this.#language, 185 | }, 186 | }); 187 | 188 | // initialize 189 | this.textContent = ''; 190 | this.#a11yName.textContent = this.#label; 191 | this.#slots.name.hidden = !this.#label; 192 | this.#slots.copyButton.hidden = !this.#controls; 193 | this.#codeBlock.textContent = ''; 194 | this.#codeBlock.insertAdjacentHTML('afterbegin', markup); 195 | this.append(this.#a11yName); 196 | this.append(this.#copyButton); 197 | this.append(this.#codeWrap); 198 | this.#observer.observe(this, { 199 | childList: true, 200 | }); 201 | } 202 | 203 | /** @return - Syntax Highlighted Source Code */ 204 | get value(): string { 205 | return this.#value; 206 | } 207 | 208 | set value(src: unknown) { 209 | this.#value = String(src); 210 | this.#render(); 211 | } 212 | 213 | /** 214 | * The accessible name of code block 215 | * @return - The value of the label attribute 216 | */ 217 | get label(): string { 218 | return this.#label; 219 | } 220 | 221 | set label(value: unknown) { 222 | if ( 223 | this.#label === value || 224 | (this.#label === '' && 225 | this.getAttribute('label') === null && 226 | value === null) 227 | ) { 228 | return; 229 | } 230 | 231 | if (value === null) { 232 | this.#label = ''; 233 | this.removeAttribute('label'); 234 | } else { 235 | this.#label = String(value); 236 | this.setAttribute('label', this.#label); 237 | } 238 | 239 | this.#render(); 240 | } 241 | 242 | /** 243 | * Language name 244 | * @return - The value of the language attribute 245 | */ 246 | get language(): string { 247 | return this.#language; 248 | } 249 | 250 | set language(value: unknown) { 251 | if ( 252 | this.#language === value || 253 | (this.#language === '' && 254 | this.getAttribute('language') === null && 255 | value === null) 256 | ) { 257 | return; 258 | } 259 | 260 | if (value === null) { 261 | this.#language = ''; 262 | this.removeAttribute('language'); 263 | } else { 264 | this.#language = String(value); 265 | this.setAttribute('language', this.#language); 266 | } 267 | 268 | this.#render(); 269 | } 270 | 271 | /** 272 | * The flag to display the UI 273 | * @return - With or without controls attribute 274 | * */ 275 | get controls(): boolean { 276 | return this.#controls; 277 | } 278 | 279 | set controls(value: boolean) { 280 | if (this.#controls === value) { 281 | return; 282 | } 283 | 284 | this.#controls = value; 285 | 286 | if (this.#controls) { 287 | this.setAttribute('controls', ''); 288 | } else { 289 | this.removeAttribute('controls'); 290 | } 291 | 292 | this.#render(); 293 | } 294 | 295 | set notrim(value: boolean) { 296 | if (this.#notrim === value) { 297 | return; 298 | } 299 | 300 | this.#notrim = value; 301 | 302 | if (this.#notrim) { 303 | this.setAttribute('notrim', ''); 304 | } else { 305 | this.removeAttribute('notrim'); 306 | } 307 | 308 | this.#render(); 309 | } 310 | 311 | static get observedAttributes(): string[] { 312 | return ['label', 'language', 'controls', 'notrim']; 313 | } 314 | 315 | attributeChangedCallback( 316 | attrName: string, 317 | oldValue: string, 318 | newValue: string 319 | ): void { 320 | if (oldValue === newValue) { 321 | return; 322 | } 323 | 324 | // When the value of the attribute being observed changes, 325 | // pass value to accessors. 326 | switch (attrName) { 327 | // string 328 | case 'label': 329 | case 'language': 330 | this[attrName] = newValue; 331 | 332 | break; 333 | 334 | // boolean 335 | case 'controls': 336 | case 'notrim': 337 | this[attrName] = typeof newValue === 'string'; 338 | } 339 | } 340 | 341 | connectedCallback(): void { 342 | if (this.#rendered === false && this.#value === '') { 343 | this.#value = this.textContent || ''; 344 | } 345 | 346 | this.#rendered = true; 347 | this.#render(); 348 | } 349 | 350 | constructor() { 351 | super(); 352 | 353 | /* ------------------------------------------------------------------------- 354 | * Setup DOM contents 355 | * ---------------------------------------------------------------------- */ 356 | /** Container of accessible names (label attribute values). */ 357 | const a11yName = (() => { 358 | const span = document.createElement('span'); 359 | 360 | span.slot = 'name'; 361 | span.textContent = this.getAttribute('label') || ''; 362 | 363 | return span; 364 | })(); 365 | const copyButton = (() => { 366 | const button = document.createElement('button'); 367 | 368 | button.type = 'button'; 369 | button.slot = 'copy-button'; 370 | button.textContent = 'Copy'; 371 | button.setAttribute('aria-live', 'polite'); 372 | button.addEventListener('click', this.#onClickButton); 373 | 374 | return button; 375 | })(); 376 | const codeElm = (() => { 377 | const code = document.createElement('code'); 378 | 379 | code.tabIndex = 0; 380 | code.className = 'hljs'; // TODO: Make it variable 381 | 382 | return code; 383 | })(); 384 | const preElm = (() => { 385 | const pre = document.createElement('pre'); 386 | 387 | pre.slot = 'code'; 388 | pre.append(codeElm); 389 | 390 | return pre; 391 | })(); 392 | 393 | /* ------------------------------------------------------------------------- 394 | * Setup Shadow DOM contents 395 | * ---------------------------------------------------------------------- */ 396 | /** 397 | * The container of minimum text that will be read even 398 | * if the accessible name (label attribute value) is omitted. 399 | */ 400 | const a11yNamePrefix = (() => { 401 | const span = document.createElement('span'); 402 | 403 | span.id = 'semantics'; 404 | span.hidden = true; 405 | span.textContent = 'Code Block'; 406 | 407 | return span; 408 | })(); 409 | const container = (() => { 410 | const div = document.createElement('div'); 411 | 412 | div.append(...Object.values(this.#slots)); 413 | div.setAttribute('role', 'group'); 414 | div.setAttribute('aria-labelledby', 'semantics name'); 415 | 416 | return div; 417 | })(); 418 | const shadowRoot = this.attachShadow({ 419 | mode: 'closed', 420 | }); 421 | 422 | shadowRoot.append(a11yNamePrefix); 423 | shadowRoot.append(container); 424 | 425 | /* ------------------------------------------------------------------------- 426 | * Hard private props initialize 427 | * ---------------------------------------------------------------------- */ 428 | this.#value = (this.textContent || '') 429 | .replace(/^\n/, '') 430 | .replace(/\n$/, ''); 431 | this.#label = a11yName.textContent || ''; 432 | this.#language = this.getAttribute('language') || ''; 433 | this.#controls = this.getAttribute('controls') !== null; 434 | this.#notrim = this.getAttribute('notrim') !== null; 435 | this.#a11yName = a11yName; 436 | this.#copyButton = copyButton; 437 | this.#codeBlock = codeElm; 438 | this.#codeWrap = preElm; 439 | } 440 | } 441 | 442 | // Protect constructor names from minify 443 | Object.defineProperty(HTMLCodeBlockElement, 'name', { 444 | value: 'HTMLCodeBlockElement', 445 | }); 446 | -------------------------------------------------------------------------------- /src/effects/add-style.ts: -------------------------------------------------------------------------------- 1 | // Inserts the style element into the page for 2 | 3 | import styleSheet from '@/stylesheet'; 4 | 5 | // the default style of the code-block element. 6 | const style = document.createElement('style'); 7 | const link = document.querySelector('head link, head style'); 8 | 9 | style.textContent = styleSheet; 10 | style.dataset.id = 'html-code-block-element'; 11 | 12 | if (link) { 13 | link.before(style); 14 | } else { 15 | document.head.append(style); 16 | } 17 | -------------------------------------------------------------------------------- /src/index.all.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js'; 2 | import CustomElementConstructor from '@/class/HTMLCodeBlockElement'; 3 | import {createHighlightCallback} from '@/utils/createHighlightCallback'; 4 | import '@/effects/add-style'; 5 | 6 | CustomElementConstructor.highlight = createHighlightCallback(hljs); 7 | customElements.define('code-block', CustomElementConstructor); 8 | 9 | export const HTMLCodeBlockElement = CustomElementConstructor; 10 | -------------------------------------------------------------------------------- /src/index.common.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js/lib/common'; 2 | import CustomElementConstructor from '@/class/HTMLCodeBlockElement'; 3 | import {createHighlightCallback} from '@/utils/createHighlightCallback'; 4 | import '@/effects/add-style'; 5 | 6 | CustomElementConstructor.highlight = createHighlightCallback(hljs); 7 | customElements.define('code-block', CustomElementConstructor); 8 | 9 | export const HTMLCodeBlockElement = CustomElementConstructor; 10 | -------------------------------------------------------------------------------- /src/index.core.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js/lib/core'; 2 | import CustomElementConstructor from '@/class/HTMLCodeBlockElement'; 3 | import {createHighlightCallback} from '@/utils/createHighlightCallback'; 4 | import '@/effects/add-style'; 5 | 6 | CustomElementConstructor.highlight = createHighlightCallback(hljs); 7 | customElements.define('code-block', CustomElementConstructor); 8 | 9 | export const HTMLCodeBlockElement = CustomElementConstructor; 10 | -------------------------------------------------------------------------------- /src/manual.ts: -------------------------------------------------------------------------------- 1 | import CustomElementConstructor from '@/class/HTMLCodeBlockElement'; 2 | import {createHighlightCallback as createHighlightCallback_} from '@/utils/createHighlightCallback'; 3 | 4 | export type CodeBlockProps = React.DetailedHTMLProps< 5 | React.HTMLAttributes, 6 | HTMLPreElement 7 | > & { 8 | /** The accessible name of code block */ 9 | label?: string | undefined; 10 | /** The Language name */ 11 | language?: string | undefined; 12 | /** The flag to display the UI */ 13 | controls?: boolean; 14 | }; 15 | 16 | export const HTMLCodeBlockElement = CustomElementConstructor; 17 | export const createHighlightCallback = createHighlightCallback_; 18 | -------------------------------------------------------------------------------- /src/stylesheet.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | code-block { 3 | position: relative; 4 | margin: 1em 0; 5 | display: block; 6 | font-size: 80%; 7 | font-family: Consolas, Monaco, monospace; 8 | } 9 | code-block span[slot="name"] { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | z-index: 0; 14 | padding: 0 5px; 15 | max-width: 90%; 16 | color: #fff; 17 | white-space: pre; 18 | line-height: 1.5; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | background: #75758a; 22 | box-sizing: border-box; 23 | } 24 | code-block button { 25 | all: unset; 26 | outline: revert; 27 | position: absolute; 28 | right: 0; 29 | top: 0; 30 | z-index: 1; 31 | padding: 10px; 32 | display: block; 33 | font-family: inherit; 34 | color: #fff; 35 | opacity: 0; 36 | mix-blend-mode: exclusion; 37 | } 38 | 39 | code-block:hover button, 40 | code-block button:focus { 41 | opacity: 1; 42 | } 43 | 44 | code-block pre, 45 | code-block code { 46 | font-family: inherit; 47 | } 48 | code-block pre { 49 | margin: 0; 50 | } 51 | code-block code { 52 | padding: 1em; 53 | display: block; 54 | font-size: 100%; 55 | overflow-x: auto; 56 | } 57 | code-block[label]:not([label=""]) pre code { 58 | padding-top: 2em; 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/utils/createHighlightCallback.ts: -------------------------------------------------------------------------------- 1 | import {HLJSApi} from 'highlight.js'; 2 | import {EndgineFunction} from '@/class/HTMLCodeBlockElement'; 3 | 4 | /** 5 | * Callback maker for highlight.js 6 | * @param endgine - A library for performing syntax highlighting. 7 | * @return - A function for HTMLCodeBlockElement.highlight 8 | */ 9 | export const createHighlightCallback = 10 | (endgine: HLJSApi): EndgineFunction => 11 | ({src, options}) => { 12 | const hljs: HLJSApi = endgine; 13 | 14 | if ( 15 | // Verifying the existence of a language 16 | options?.language && 17 | hljs.getLanguage(options.language) 18 | ) { 19 | return { 20 | markup: hljs.highlight(src, options).value, 21 | }; 22 | } 23 | 24 | return { 25 | markup: hljs.highlightAuto(src).value, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /test/HTMLCodeBlockElement.test.ts: -------------------------------------------------------------------------------- 1 | import HTMLCodeBlockElement from '../src/class/HTMLCodeBlockElement'; 2 | 3 | // highlight endgine mock 4 | HTMLCodeBlockElement.highlight = function hoge({src}) { 5 | return { 6 | markup: src, 7 | }; 8 | }; 9 | 10 | customElements.define('code-block', HTMLCodeBlockElement); 11 | 12 | describe('Props', () => { 13 | test('Default', () => { 14 | const cbElm = new HTMLCodeBlockElement(); 15 | 16 | expect(cbElm.controls).toBe(false); 17 | expect(cbElm.label).toBe(''); 18 | expect(cbElm.language).toBe(''); 19 | expect(cbElm.value).toBe(''); 20 | }); 21 | 22 | test('With controls attributes', () => { 23 | const cbElm = new HTMLCodeBlockElement(); 24 | 25 | expect(cbElm.getAttribute('controls')).toBeNull(); 26 | cbElm.controls = true; 27 | expect(cbElm.getAttribute('controls')).toBe(''); 28 | cbElm.controls = false; 29 | expect(cbElm.getAttribute('controls')).toBeNull(); 30 | }); 31 | 32 | test('With notrim attributes', () => { 33 | const cbElm = new HTMLCodeBlockElement(); 34 | 35 | expect(cbElm.getAttribute('notrim')).toBeNull(); 36 | cbElm.notrim = true; 37 | expect(cbElm.getAttribute('notrim')).toBe(''); 38 | cbElm.notrim = false; 39 | expect(cbElm.getAttribute('notrim')).toBeNull(); 40 | }); 41 | 42 | test('With label attributes', () => { 43 | const cbElm = new HTMLCodeBlockElement(); 44 | 45 | expect(cbElm.getAttribute('label')).toBeNull(); 46 | cbElm.label = 'label text'; 47 | expect(cbElm.getAttribute('label')).toBe('label text'); 48 | cbElm.label = ''; 49 | expect(cbElm.getAttribute('label')).toBe(''); 50 | cbElm.label = null; 51 | expect(cbElm.getAttribute('label')).toBeNull(); 52 | }); 53 | 54 | test('With language attributes', () => { 55 | const cbElm = new HTMLCodeBlockElement(); 56 | 57 | expect(cbElm.getAttribute('language')).toBeNull(); 58 | cbElm.language = 'js'; 59 | expect(cbElm.getAttribute('language')).toBe('js'); 60 | cbElm.language = ''; 61 | expect(cbElm.getAttribute('language')).toBe(''); 62 | cbElm.language = null; 63 | expect(cbElm.getAttribute('language')).toBeNull(); 64 | }); 65 | 66 | test('value prop with blank line', () => { 67 | const cbElm = new HTMLCodeBlockElement(); 68 | const value = ` 69 | a 70 | b 71 | c 72 | `; 73 | 74 | expect(cbElm.value).toBe(''); 75 | cbElm.value = value; 76 | expect(cbElm.value).toBe(value); 77 | }); 78 | }); 79 | 80 | describe('Render', () => { 81 | document.body.innerHTML = `abc`; 82 | 83 | const cb = document.body.firstElementChild! as HTMLCodeBlockElement; 84 | 85 | test('connectedCallback', () => { 86 | expect(cb.children.length).toBe(3); 87 | }); 88 | test('Attributes', () => { 89 | expect(cb.children[0].tagName.toLowerCase()).toBe('span'); 90 | expect(cb.children[1].tagName.toLowerCase()).toBe('button'); 91 | expect(cb.children[2].tagName.toLowerCase()).toBe('pre'); 92 | expect(cb.children[2].children[0].tagName.toLowerCase()).toBe('code'); 93 | }); 94 | 95 | // TODO: Write tests of MutationObserver 96 | 97 | // test('value', () => { 98 | // const value = ` 99 | // a 100 | // b 101 | // c 102 | // `; 103 | 104 | // expect(cb.value).toBe('abc'); 105 | // cb.value = 'abcd'; 106 | // expect(cb.value).toBe('abcd'); 107 | // cb.value = value; 108 | // expect(cb.value).toBe(value); 109 | // }); 110 | // test('Clipboard', () => { 111 | // document.body.innerHTML = `abc`; 112 | // document.body.firstElementChild?.querySelector('button')?.click(); 113 | 114 | // expect(navigator.clipboard.readText()).toBe('abc'); 115 | // }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/HTMLCodeBlockElement.throw.test.ts: -------------------------------------------------------------------------------- 1 | import HTMLCodeBlockElement from '../src/class/HTMLCodeBlockElement'; 2 | 3 | describe('Error', () => { 4 | test('No Endgine', () => { 5 | expect(() => HTMLCodeBlockElement.highlight({src: 'sample'})).toThrowError( 6 | 'The syntax highlighting engine is not set to `HTMLCodeBlockElement.highlight`.' 7 | ); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "declaration": true, 8 | "pretty": true, 9 | "newLine": "lf", 10 | "outDir": "dist", 11 | "esModuleInterop": true, 12 | "useDefineForClassFields": true, 13 | "types": ["jest", "node"], 14 | "jsx": "react-jsx", 15 | "baseUrl": "./", 16 | "paths": { 17 | "@/*": ["src/*"] 18 | } 19 | }, 20 | "include": ["src/**/*.ts"], 21 | "exclude": ["node_modules", "**/*.spec.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | const {LicenseWebpackPlugin} = require('license-webpack-plugin'); 4 | const path = require('path'); 5 | const banner = (({name, version, author, license}) => { 6 | return ` 7 | /*! 8 | * ${name} v${version} 9 | * author: ${author} 10 | * license: ${license} 11 | */ 12 | `; 13 | })(require('./package.json')); 14 | 15 | module.exports = { 16 | entry: { 17 | 'html-code-block-element.core.min': './dist/index.core.js', 18 | 'html-code-block-element.common.min': './dist/index.common.js', 19 | 'html-code-block-element.all.min': './dist/index.all.js', 20 | }, 21 | output: { 22 | path: path.join(__dirname, 'lib'), 23 | library: 'HTMLCodeBlockElement', 24 | libraryExport: 'HTMLCodeBlockElement', 25 | libraryTarget: 'window', 26 | }, 27 | resolve: { 28 | extensions: ['.ts', '.js'], 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.(ts|js)$/, 34 | loader: 'babel-loader', 35 | }, 36 | ], 37 | }, 38 | performance: { 39 | maxEntrypointSize: 1200000, 40 | maxAssetSize: 1200000, 41 | }, 42 | devServer: { 43 | open: true, 44 | static: [path.resolve(__dirname, 'demo'), path.resolve(__dirname, 'lib')], 45 | }, 46 | plugins: [ 47 | new webpack.BannerPlugin({ 48 | banner, 49 | raw: true, 50 | entryOnly: true, 51 | }), 52 | new LicenseWebpackPlugin(), 53 | ], 54 | optimization: { 55 | minimizer: [ 56 | new TerserPlugin({ 57 | extractComments: false, 58 | terserOptions: { 59 | keep_classnames: true, 60 | }, 61 | }), 62 | ], 63 | }, 64 | }; 65 | --------------------------------------------------------------------------------