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


--------------------------------------------------------------------------------
/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 | 


--------------------------------------------------------------------------------
/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/__fixtures__/basic.expected.html:
--------------------------------------------------------------------------------
1 | 

2 |   const foo = 'foo'
3 |   console.log(foo)
4 | 
-------------------------------------------------------------------------------- /test/__fixtures__/basic.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 | 
-------------------------------------------------------------------------------- /test/__fixtures__/existingClass.html: -------------------------------------------------------------------------------- 1 |

2 |   const foo = 'foo'
3 |   console.log(foo)
4 | 
-------------------------------------------------------------------------------- /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/__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__/nohighlight.expected.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/__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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "baseUrl": "../", 6 | "esModuleInterop": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------