├── .prettierignore ├── .gitignore ├── .husky ├── pre-commit └── commit-msg ├── test ├── __fixtures__ │ ├── basic.html │ ├── existingClass.html │ ├── nohighlight.html │ ├── nohighlight.expected.html │ ├── nested.html │ ├── basic.expected.html │ ├── existingClass.expected.html │ └── nested.expected.html ├── tsconfig.json ├── __mocks__ │ └── highlight.js.ts └── test.ts ├── .github ├── dependabot.yml └── workflows │ └── nodejs.yml ├── tsconfig.json ├── LICENSE.md ├── src └── index.ts ├── CHANGELOG.md ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | test/**/*.html -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | coverage/ 3 | dist/ 4 | node_modules/ 5 | *.log -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn format --staged 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /test/__fixtures__/basic.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 | 
-------------------------------------------------------------------------------- /test/__fixtures__/existingClass.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 | 
-------------------------------------------------------------------------------- /test/__fixtures__/nohighlight.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 | 
-------------------------------------------------------------------------------- /test/__fixtures__/nohighlight.expected.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 | 
-------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "baseUrl": "../", 6 | "esModuleInterop": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/__fixtures__/nested.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 |   
5 |     const bar = 'bar'
6 |     console.log(bar)
7 |   
8 | 
9 | -------------------------------------------------------------------------------- /test/__fixtures__/basic.expected.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 | 
-------------------------------------------------------------------------------- /test/__fixtures__/existingClass.expected.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 | 
-------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "22:00" 8 | open-pull-requests-limit: 10 9 | versioning-strategy: widen 10 | ignore: 11 | - dependency-name: tslint 12 | versions: 13 | - "> 5.17.0" 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "typeRoots": ["@types", "node_modules/@types"] 11 | }, 12 | "exclude": ["dist", "test"] 13 | } 14 | -------------------------------------------------------------------------------- /test/__fixtures__/nested.expected.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 |   
5 |     const bar = 'bar'
6 |     console.log(bar)
7 |   
8 | 
9 | -------------------------------------------------------------------------------- /test/__mocks__/highlight.js.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js' 2 | 3 | const configure = jest.fn(hljs.configure) 4 | const highlight = jest.fn(hljs.highlight) 5 | const highlightAuto = jest.fn(hljs.highlightAuto) 6 | 7 | function mockClear(): void { 8 | configure.mockClear() 9 | highlight.mockClear() 10 | highlightAuto.mockClear() 11 | } 12 | 13 | export default { 14 | configure, 15 | highlight, 16 | highlightAuto, 17 | mockClear, 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2018 Casey Webb (https://caseyWebb.xyz) 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [12.x, 14.x, 16.x, 17.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: yarn install, build, lint, and test 20 | run: | 21 | yarn install 22 | yarn build 23 | yarn lint 24 | yarn test 25 | env: 26 | CI: true 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v2 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import hljs, { HLJSOptions } from 'highlight.js' 2 | import { Node } from 'posthtml' 3 | 4 | export type Options = Partial & { 5 | inline?: boolean 6 | } 7 | 8 | export default function createHighlightPlugin( 9 | config: Options = {} 10 | ): (tree: Node) => void { 11 | return function highlightPlugin(tree: Node): void { 12 | const highlightCodeTags = (node: Node): Node[] => 13 | tree.match.call(node, { tag: 'code' }, highlightNode) 14 | 15 | hljs.configure(config) 16 | 17 | if (config.inline) { 18 | highlightCodeTags(tree) 19 | } else { 20 | tree.match({ tag: 'pre' }, highlightCodeTags) 21 | } 22 | } 23 | } 24 | 25 | function highlightNode(node: Node): Node { 26 | const attrs = node.attrs || {} 27 | const classList = `${attrs.class || ''} hljs`.trimLeft() 28 | if (classList.includes('nohighlight')) return node 29 | const lang = getExplicitLanguage(classList) 30 | attrs.class = classList 31 | node.attrs = attrs 32 | if (node.content) { 33 | node.content = node.content.map((c) => mapContentOrNode(c, lang)) 34 | } 35 | return node 36 | } 37 | 38 | function getExplicitLanguage(classList: string): string | undefined { 39 | const matches = /(?:lang|language)-(\w*)/.exec(classList) 40 | return matches === null ? void 0 : matches[1] 41 | } 42 | 43 | function mapContentOrNode( 44 | contentOrNode: string | Node, 45 | lang?: string 46 | ): string | Node { 47 | if (typeof contentOrNode === 'string') { 48 | if (lang) { 49 | return hljs.highlight(contentOrNode, { language: lang }).value 50 | } else { 51 | return hljs.highlightAuto(contentOrNode).value 52 | } 53 | } else { 54 | highlightNode(contentOrNode) 55 | return contentOrNode 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | import { promisify } from 'util' 4 | 5 | import hljs from 'highlight.js' 6 | 7 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 8 | // @ts-ignore 9 | import posthtml from 'posthtml' 10 | 11 | import plugin from '../src' 12 | 13 | const readFile = promisify(fs.readFile) 14 | const fixtures = path.join(__dirname, '__fixtures__') 15 | 16 | beforeEach(() => (hljs as unknown as jest.Mock).mockClear()) 17 | 18 | test('basic', createFixtureTest('basic')) 19 | test('nested', createFixtureTest('nested')) 20 | test( 21 | 'does not highlight tags with `nohighlight` class', 22 | createFixtureTest('nohighlight') 23 | ) 24 | test('appends hljs to existing class list', createFixtureTest('existingClass')) 25 | 26 | test('configures highlight.js with supplied configuration', async () => { 27 | const source = '
// ambiguous
' 28 | const config = { languages: ['javascript', 'typescript'] } 29 | 30 | await posthtml([plugin(config)]).process(source) 31 | 32 | expect(hljs.configure).lastCalledWith(config) 33 | }) 34 | 35 | test('only highlights inline code blocks if options.inline', async () => { 36 | const source = '// ambiguous' 37 | 38 | await posthtml([plugin()]).process(source) 39 | await posthtml([plugin({ inline: true })]).process(source) 40 | 41 | expect(hljs.highlightAuto).toHaveBeenCalledTimes(1) 42 | }) 43 | 44 | test('uses with language specified via language-*', async () => { 45 | const source = 46 | '
// ambiguous
' 47 | 48 | await posthtml([plugin()]).process(source) 49 | 50 | expect(hljs.highlight).lastCalledWith('// ambiguous', { 51 | language: 'javascript', 52 | }) 53 | }) 54 | 55 | test('uses with language specified via lang-*', async () => { 56 | const source = '
// ambiguous
' 57 | 58 | await posthtml([plugin()]).process(source) 59 | 60 | expect(hljs.highlight).lastCalledWith('// ambiguous', { 61 | language: 'typescript', 62 | }) 63 | }) 64 | 65 | function createFixtureTest(name: string) { 66 | return async () => { 67 | const source = await readFile(path.join(fixtures, `${name}.html`), 'utf8') 68 | const [expected, { html: actual }] = await Promise.all([ 69 | readFile(path.join(fixtures, `${name}.expected.html`), 'utf8'), 70 | posthtml([plugin()]).process(source), 71 | ]) 72 | 73 | expect(actual).toBe(expected) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [3.0.0](https://github.com/posthtml/posthtml-highlight/compare/v1.1.1...v3.0.0) (2021-12-30) 6 | 7 | ### ⚠ BREAKING CHANGES 8 | 9 | - highlight.js language aliases have changed 10 | (https://github.com/highlightjs/highlight.js/blob/main/VERSION_11_UPGRADE.md) 11 | - require Node >= 10 12 | - Drop support for Node v8 13 | 14 | ### Bug Fixes 15 | 16 | - types after update pkg ([100c667](https://github.com/posthtml/posthtml-highlight/commit/100c667f827d988d629454ef6b31325dc15c81b9)) 17 | 18 | ### build 19 | 20 | - drop support old node ([43461d4](https://github.com/posthtml/posthtml-highlight/commit/43461d4dea3aa6f38831a063af5395effc47506e)) 21 | 22 | - drop (explicit) support for Node v8 ([a093dc1](https://github.com/posthtml/posthtml-highlight/commit/a093dc13ee12450bccbb7ad7d4d5956282d825df)) 23 | - update highlight.js ([e879559](https://github.com/posthtml/posthtml-highlight/commit/e87955986239bbe1dec45b4abbe7b753f0d67d42)) 24 | 25 | ## [2.0.0](https://github.com/posthtml/posthtml-highlight/compare/v1.1.1...v2.0.0) (2020-08-25) 26 | 27 | ### ⚠ BREAKING CHANGES 28 | 29 | - require Node >= 10 30 | - Drop support for Node v8 31 | 32 | ### Bug Fixes 33 | 34 | - types after update pkg ([2a25676](https://github.com/posthtml/posthtml-highlight/commit/2a25676daa2700f67390cbfdeaed6a4e97ff27d3)) 35 | 36 | * drop (explicit) support for Node v8 ([a093dc1](https://github.com/posthtml/posthtml-highlight/commit/a093dc13ee12450bccbb7ad7d4d5956282d825df)) 37 | 38 | ### build 39 | 40 | - drop support old node ([92bcdac](https://github.com/posthtml/posthtml-highlight/commit/92bcdac0c5ed0a1379010963665167df093348fa)) 41 | 42 | ### [1.1.1](https://github.com/posthtml/posthtml-highlight/compare/v1.1.0...v1.1.1) (2019-08-30) 43 | 44 | ## [1.1.0](https://github.com/posthtml/posthtml-highlight/compare/v1.0.3...v1.1.0) (2019-08-21) 45 | 46 | ### Features 47 | 48 | - support nested tags ([a39a522](https://github.com/posthtml/posthtml-highlight/commit/a39a522)) 49 | 50 | ## 1.0.3 (June 06, 2019) 51 | 52 | - Chore: Upgrade highlight.js 9.12.0 => ^9.15.0 53 | 54 | ## 1.0.2 (Mar 28, 2018) 55 | 56 | - Fixed: Add `hljs` class to container element 57 | - Internal: Add npm lifecycle hooks to prevent oopsies 58 | 59 | ## 1.0.1 (Mar 27, 2018) 60 | 61 | - Fixed: README 62 | - Fixed: package.json => engines 63 | 64 | ## 1.0.0 (Mar 27, 2018) 65 | 66 | - Added: Initial version 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "posthtml-highlight", 3 | "description": "PostHTML Syntax Highlighting Plugin", 4 | "version": "3.0.0", 5 | "license": "WTFPL", 6 | "author": "Casey Webb (https://caseyWebb.xyz)", 7 | "bugs": "https://github.com/posthtml/posthtml-highlight/issues", 8 | "homepage": "https://github.com/posthtml/posthtml-highlight", 9 | "repository": "posthtml/posthtml-highlight", 10 | "engines": { 11 | "node": ">=12" 12 | }, 13 | "scripts": { 14 | "build": "tsc", 15 | "format": "pretty-quick", 16 | "lint": "eslint --ext .ts --ignore-path .gitignore ./", 17 | "release": "standard-version --sign && git push --follow-tags", 18 | "test": "jest", 19 | "prepare": "husky install" 20 | }, 21 | "keywords": [ 22 | "html", 23 | "posthtml", 24 | "posthtml-plugin", 25 | "syntax", 26 | "highlight", 27 | "highlighter", 28 | "highlighting", 29 | "code" 30 | ], 31 | "main": "dist", 32 | "files": [ 33 | "dist" 34 | ], 35 | "config": { 36 | "commitizen": { 37 | "path": "./node_modules/cz-conventional-changelog" 38 | } 39 | }, 40 | "commitlint": { 41 | "extends": [ 42 | "@commitlint/config-conventional" 43 | ] 44 | }, 45 | "eslintConfig": { 46 | "extends": "profiscience", 47 | "parserOptions": { 48 | "project": [ 49 | "./tsconfig.json", 50 | "./test/tsconfig.json" 51 | ] 52 | }, 53 | "rules": { 54 | "@typescript-eslint/no-use-before-define": [ 55 | "error", 56 | { 57 | "functions": false 58 | } 59 | ] 60 | } 61 | }, 62 | "jest": { 63 | "collectCoverage": true, 64 | "coveragePathIgnorePatterns": [ 65 | "/dist/", 66 | "/node_modules/" 67 | ], 68 | "coverageReporters": [ 69 | "lcov", 70 | "html" 71 | ], 72 | "moduleFileExtensions": [ 73 | "js", 74 | "ts" 75 | ], 76 | "roots": [ 77 | "src", 78 | "test" 79 | ], 80 | "testMatch": [ 81 | "**/test/*.ts" 82 | ], 83 | "testURL": "http://localhost", 84 | "transform": { 85 | "^.+\\.[tj]sx?$": "ts-jest" 86 | } 87 | }, 88 | "prettier": { 89 | "arrowParens": "always", 90 | "semi": false, 91 | "singleQuote": true 92 | }, 93 | "dependencies": { 94 | "highlight.js": "^11.3.1" 95 | }, 96 | "peerDependencies": { 97 | "posthtml": "^0.15.1" 98 | }, 99 | "devDependencies": { 100 | "@commitlint/cli": "^16.0.1", 101 | "@commitlint/config-conventional": "^16.0.0", 102 | "@types/highlight.js": "^10.1.0", 103 | "@types/jest": "^27.4.0", 104 | "@types/node": "^17.0.5", 105 | "cz-conventional-changelog": "^3.0.2", 106 | "eslint": "^8.5.0", 107 | "eslint-config-profiscience": "^7.0.1", 108 | "husky": "^7.0.0", 109 | "jest": "^27.4.5", 110 | "posthtml": "^0.16.5", 111 | "prettier": "^2.5.1", 112 | "pretty-quick": "^3.1.3", 113 | "standard-version": "^9.3.2", 114 | "ts-jest": "^27.1.2", 115 | "typescript": "^4.5.4" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostHTML Highlight Plugin 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![License][wtfpl-shield]][wtfpl] 5 | [![TypeScript][typescript-shield]][typescript] 6 | [![Build Status][build-status-shield]][build-status] 7 | [![Coverage][codecov-shield]][codecov] 8 | [![Downloads][npm-stats-shield]][npm-stats] 9 | [![Chat][gitter-shield]][gitter] 10 | 11 | Compile-time syntax highlighting for code blocks via [highlight.js][] 12 | 13 | Before: 14 | 15 | ```html 16 |

 17 |   const foo = 'foo'
 18 |   console.log(foo)
 19 | 
20 | ``` 21 | 22 | After: 23 | 24 | ```html 25 |

 26 |   const foo = 'foo'
 27 |   console.log(foo)
 28 | 
29 | ``` 30 | 31 | ## Install 32 | 33 | ``` 34 | $ yarn add -D posthtml posthtml-highlight 35 | ``` 36 | 37 | _or_ 38 | 39 | ``` 40 | $ npm i posthtml posthtml-highlight 41 | ``` 42 | 43 | If using TypeScript, additionally install `@types/highlight.js` 44 | 45 | ## Usage 46 | 47 | ```js 48 | const fs = require('fs') 49 | const posthtml = require('posthtml') 50 | const highlight = require('posthtml-highlight') 51 | 52 | const source = fs.readFileSync('./before.html') 53 | 54 | posthtml([ 55 | highlight( 56 | /* optional */ { 57 | /** 58 | * By default, only code tags wrapped in pre tags are highlighted (i.e.
)
 59 |        *
 60 |        * Set `inline: true` to highlight all code tags
 61 |        */
 62 |       inline: true,
 63 | 
 64 |       /**
 65 |        * You may also pass any highlight.js options (http://highlightjs.readthedocs.io/en/latest/api.html#configure-options)
 66 |        */
 67 |       useBR: true,
 68 |     }
 69 |   ),
 70 | ])
 71 |   .process(source)
 72 |   .then((result) => fs.writeFileSync('./after.html', result.html))
 73 | ```
 74 | 
 75 | ### Styling
 76 | 
 77 | You will also need to include a highlight.js stylesheet
 78 | 
 79 | View the available color schemes [here](https://highlightjs.org/static/demo/), then  
 80 | a) include via a [CDN](https://cdnjs.com/libraries/highlight.js)  
 81 | b) install via npm (`yarn add -D highlight.js`, `./node_modules/highlight.js/styles/*`)  
 82 | c) download via the [highlight.js repo](https://github.com/isagalaev/highlight.js/tree/master/src/styles)
 83 | 
 84 | ### Specifying a language
 85 | 
 86 | Specifying a language as per [highlight.js's usage docs][] is supported, with the caveat that you must use the `lang-*` or `language-*` prefix
 87 | 
 88 | ### Skip highlighting on a node
 89 | 
 90 | Add the `nohighlight` class as per [highlight.js's usage docs][]
 91 | 
 92 | [highlight.js]: https://highlightjs.org/
 93 | [highlight.js's usage docs]: https://highlightjs.org/usage/
 94 | [npm]: https://www.npmjs.com/package/posthtml-highlight
 95 | [npm-version-shield]: https://img.shields.io/npm/v/posthtml-highlight.svg
 96 | [npm-stats]: http://npm-stat.com/charts.html?package=posthtml-highlight&author=&from=&to=
 97 | [npm-stats-shield]: https://img.shields.io/npm/dt/posthtml-highlight.svg?maxAge=2592000
 98 | [typescript]: https://www.typescriptlang.org/
 99 | [typescript-shield]: https://img.shields.io/badge/definitions-TypeScript-blue.svg
100 | [build-status]: https://github.com/posthtml/posthtml-highlight/actions/workflows/nodejs.yml
101 | [build-status-shield]: https://img.shields.io/github/workflow/status/posthtml/posthtml-highlight/Node%20CI/master
102 | [codecov]: https://codecov.io/gh/posthtml/posthtml-highlight
103 | [codecov-shield]: https://img.shields.io/codecov/c/github/posthtml/posthtml-highlight.svg
104 | [gitter]: https://gitter.im/posthtml/posthtml
105 | [gitter-shield]: https://badges.gitter.im/posthtml/posthtml.svg
106 | [wtfpl]: ./LICENSE.md
107 | [wtfpl-shield]: https://img.shields.io/npm/l/posthtml-highlight.svg
108 | 


--------------------------------------------------------------------------------