├── .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 |
--------------------------------------------------------------------------------