├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── 1_Bug_report.md ├── dependabot.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── test ├── expect │ ├── basic.html │ ├── custom_language_default.html │ ├── custom_language_load.html │ ├── inline_code.html │ ├── invalid_language.html │ ├── preserves_classes.html │ ├── prism_ignore_attr.html │ └── prism_ignore_class.html ├── fixtures │ ├── basic.html │ ├── custom_language_default.html │ ├── custom_language_load.html │ ├── inline_code.html │ ├── invalid_language.html │ ├── preserves_classes.html │ ├── prism_ignore_attr.html │ └── prism_ignore_class.html └── test.js └── xo.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://mailviews.com'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | about: 'Report a general issue.' 4 | 5 | --- 6 | 7 | ## Problem 8 | 9 | _Describe your problem._ 10 | 11 | ## Environment 12 | 13 | - `posthtml-prism` plugin version: #.#.# 14 | - PostHTML version: #.#.# 15 | - Node.js version: #.#.# 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "22:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14, 16, 18] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm test 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Cosmin Popovici 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
19 | const foo = 'bar'
20 | console.log(foo)
21 |
22 | ```
23 |
24 | After:
25 |
26 | ```html
27 |
28 | const foo = 'bar'
29 | console.log(foo)
30 |
31 | ```
32 |
33 | ## Install
34 |
35 | ```
36 | $ npm i posthtml posthtml-prism
37 | ```
38 |
39 | ## Usage
40 |
41 | ```js
42 | const fs = require('fs')
43 | const posthtml = require('posthtml')
44 | const highlight = require('posthtml-prism')
45 |
46 | const source = fs.readFileSync('./before.html')
47 |
48 | posthtml([
49 | highlight({ inline: true })
50 | ])
51 | .process(source)
52 | .then(result => fs.writeFileSync('./after.html', result.html))
53 | ```
54 |
55 | ## Options
56 |
57 | ### inline
58 |
59 | Type: `boolean`\
60 | Default: `false`
61 |
62 | By default, only `` tags wrapped in `` tags are highlighted.
63 |
64 | Pass in `inline: true` to highlight all code tags.
65 |
66 | ## Styling
67 |
68 | You will also need to include a Prism theme stylesheet in your HTML.
69 |
70 | See [PrismJS/prism-themes](https://github.com/PrismJS/prism-themes) for all available themes.
71 |
72 | ## Languages
73 |
74 | By default, Prism loads the following languages: `markup`, `css`, `clike`, and `javascript`.
75 |
76 | You can specify the language to be used for highlighting your code, by adding a `language-*` or `lang-*` class to the `` tag:
77 |
78 | ```html
79 |
80 |
81 | $app->post('framework/{id}', function($framework) {
82 | $this->dispatch(new Energy($framework));
83 | });
84 |
85 |
86 | ```
87 |
88 | ### Skip highlighting on a node
89 |
90 | You can skip highlighting on a node in two ways:
91 |
92 | 1. add a `prism-ignore` attribute on the node:
93 | ```html
94 |
95 | ...
96 |
97 | ```
98 |
99 | 2. or, add a `prism-ignore` class:
100 | ```html
101 |
102 | ...
103 |
104 | ```
105 |
106 | In both cases, the `prism-ignore` attribute/class will be removed and highlighting will be skipped.
107 |
108 | [npm]: https://www.npmjs.com/package/posthtml-prism
109 | [npm-version-shield]: https://img.shields.io/npm/v/posthtml-prism.svg
110 | [npm-stats]: http://npm-stat.com/charts.html?package=posthtml-prism&author=&from=&to=
111 | [npm-stats-shield]: https://img.shields.io/npm/dt/posthtml-prism.svg?maxAge=2592000
112 | [github-ci]: https://github.com/posthtml/posthtml-prism/actions
113 | [github-ci-shield]: https://github.com/posthtml/posthtml-prism/actions/workflows/nodejs.yml/badge.svg
114 | [license]: ./license
115 | [license-shield]: https://img.shields.io/npm/l/posthtml-prism.svg
116 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Prism = require('prismjs');
4 | const {render} = require('posthtml-render');
5 | const loadLanguages = require('prismjs/components/');
6 |
7 | function createPrismPlugin(options) {
8 | return function (tree) {
9 | const highlightCodeTags = node => tree.match.call(node, {tag: 'code'}, highlightNode);
10 |
11 | if (options.inline) {
12 | highlightCodeTags(tree);
13 | } else {
14 | tree.match({tag: 'pre'}, highlightCodeTags);
15 | }
16 | };
17 | }
18 |
19 | function highlightNode(node) {
20 | const attrs = node.attrs || {};
21 | const classList = `${attrs.class || ''}`.trimStart();
22 |
23 | if ('prism-ignore' in attrs) {
24 | delete node.attrs['prism-ignore'];
25 | return node;
26 | }
27 |
28 | if (classList.includes('prism-ignore')) {
29 | node.attrs.class = node.attrs.class.replace('prism-ignore', '').trim();
30 | return node;
31 | }
32 |
33 | const lang = getExplicitLanguage(classList);
34 |
35 | if (lang && !classList.includes(`language-${lang}`)) {
36 | attrs.class = `${classList || ''} language-${lang}`.trimStart();
37 | }
38 |
39 | node.attrs = attrs;
40 |
41 | if (node.content) {
42 | const html = (node.content[0].tag && !node.content[0].content) ? `<${node.content[0].tag}>` : render(node.content);
43 |
44 | node.content = mapStringOrNode(html, lang);
45 | }
46 |
47 | return node;
48 | }
49 |
50 | function mapStringOrNode(stringOrNode, lang = null) {
51 | if (typeof stringOrNode === 'string') {
52 | if (lang) {
53 | if (!Object.keys(Prism.languages).includes(lang)) {
54 | loadLanguages.silent = true;
55 | loadLanguages([lang]);
56 | }
57 |
58 | return Prism.highlight(stringOrNode, Prism.languages[lang], lang);
59 | }
60 |
61 | return Prism.highlight(stringOrNode, Prism.languages.markup, 'markup');
62 | }
63 |
64 | highlightNode(stringOrNode);
65 | return stringOrNode;
66 | }
67 |
68 | function getExplicitLanguage(classList) {
69 | const matches = classList.match(/(?:lang|language)-(\w*)/);
70 | return matches === null ? null : matches[1];
71 | }
72 |
73 | module.exports = options => {
74 | options = options || {};
75 | options.inline = options.inline || false;
76 |
77 | return function (tree) {
78 | return createPrismPlugin(options)(tree);
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "posthtml-prism",
3 | "description": "PostHTML code syntax highlighting with Prism",
4 | "version": "2.0.1",
5 | "license": "MIT",
6 | "author": "Cosmin Popovici (https://github.com/cossssmin)",
7 | "bugs": "https://github.com/posthtml/posthtml-prism/issues",
8 | "homepage": "https://github.com/posthtml/posthtml-prism",
9 | "repository": "posthtml/posthtml-prism",
10 | "main": "index.js",
11 | "files": [
12 | "index.js"
13 | ],
14 | "engines": {
15 | "node": ">=14.0.0"
16 | },
17 | "scripts": {
18 | "test": "c8 ava",
19 | "pretest": "xo",
20 | "release": "np"
21 | },
22 | "keywords": [
23 | "html",
24 | "posthtml",
25 | "posthtml-plugin",
26 | "prism",
27 | "syntax",
28 | "highlight",
29 | "code",
30 | "pre"
31 | ],
32 | "dependencies": {
33 | "prismjs": "^1.19.0"
34 | },
35 | "devDependencies": {
36 | "ava": "^5.2.0",
37 | "np": "^10.0.0",
38 | "c8": "^9.1.0",
39 | "posthtml": "^0.16.4",
40 | "posthtml-render": "^3.0.0",
41 | "xo": "^0.54.2"
42 | },
43 | "peerDependencies": {
44 | "posthtml-render": "^3.0.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/test/expect/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <div>
4 | <p>Lorem ipsum</p>
5 | </div>
6 |
7 |
--------------------------------------------------------------------------------
/test/expect/custom_language_default.html:
--------------------------------------------------------------------------------
1 |
2 | const foo = 'foo'
3 | console.log(foo)
4 |
--------------------------------------------------------------------------------
/test/expect/custom_language_load.html:
--------------------------------------------------------------------------------
1 |
2 | $app->post('framework/{id}', function($framework) {
3 | $this->dispatch(new Energy($framework));
4 | });
5 |
6 |
--------------------------------------------------------------------------------
/test/expect/inline_code.html:
--------------------------------------------------------------------------------
1 | The <div>
is the generic container for flow content.
--------------------------------------------------------------------------------
/test/expect/invalid_language.html:
--------------------------------------------------------------------------------
1 |
2 | const foo = 'foo'
3 | console.log(foo)
4 |
--------------------------------------------------------------------------------
/test/expect/preserves_classes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <div>
4 | <p>Lorem ipsum</p>
5 | </div>
6 |
7 |
--------------------------------------------------------------------------------
/test/expect/prism_ignore_attr.html:
--------------------------------------------------------------------------------
1 |
2 | const foo = 'foo'
3 | console.log(foo)
4 |
--------------------------------------------------------------------------------
/test/expect/prism_ignore_class.html:
--------------------------------------------------------------------------------
1 |
2 | const foo = 'foo'
3 | console.log(foo)
4 |
--------------------------------------------------------------------------------
/test/fixtures/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Lorem ipsum
5 |
6 |
7 |
--------------------------------------------------------------------------------
/test/fixtures/custom_language_default.html:
--------------------------------------------------------------------------------
1 |
2 | const foo = 'foo'
3 | console.log(foo)
4 |
--------------------------------------------------------------------------------
/test/fixtures/custom_language_load.html:
--------------------------------------------------------------------------------
1 |
2 | $app->post('framework/{id}', function($framework) {
3 | $this->dispatch(new Energy($framework));
4 | });
5 |
--------------------------------------------------------------------------------
/test/fixtures/inline_code.html:
--------------------------------------------------------------------------------
1 | The is the generic container for flow content.
--------------------------------------------------------------------------------
/test/fixtures/invalid_language.html:
--------------------------------------------------------------------------------
1 |
2 | const foo = 'foo'
3 | console.log(foo)
4 |
--------------------------------------------------------------------------------
/test/fixtures/preserves_classes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Lorem ipsum
5 |
6 |
7 |
--------------------------------------------------------------------------------
/test/fixtures/prism_ignore_attr.html:
--------------------------------------------------------------------------------
1 |
2 | const foo = 'foo'
3 | console.log(foo)
4 |
--------------------------------------------------------------------------------
/test/fixtures/prism_ignore_class.html:
--------------------------------------------------------------------------------
1 |
2 | const foo = 'foo'
3 | console.log(foo)
4 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const {readFileSync} = require('fs');
3 | const test = require('ava');
4 | const posthtml = require('posthtml');
5 | const highlight = require('..');
6 |
7 | const fixture = file => readFileSync(path.join(__dirname, 'fixtures', `${file}.html`), 'utf8');
8 | const expect = file => readFileSync(path.join(__dirname, 'expect', `${file}.html`), 'utf8');
9 |
10 | const clean = html => html.replace(/[^\S\r\n]+$/gm, '').trim();
11 |
12 | const process = (t, name, options, log = false) => {
13 | return posthtml([highlight(options)])
14 | .process(fixture(name))
15 | .then(result => log ? console.log(result.html) : clean(result.html))
16 | .then(html => t.is(html, expect(name).trim()));
17 | };
18 |
19 | test('Highlights tags inside tags', t => {
20 | return process(t, 'basic');
21 | });
22 |
23 | test('Highlights inline tags', t => {
24 | return process(t, 'inline_code', {inline: true});
25 | });
26 |
27 | test('Ignores blocks with prism-ignore attribute', t => {
28 | return process(t, 'prism_ignore_attr');
29 | });
30 |
31 | test('Ignores blocks with prism-ignore class', t => {
32 | return process(t, 'prism_ignore_class');
33 | });
34 |
35 | test('Highlights block with one of the default languages specified', t => {
36 | return process(t, 'custom_language_default');
37 | });
38 |
39 | test('Loads custom language and highlights block', t => {
40 | return process(t, 'custom_language_load');
41 | });
42 |
43 | test('Preserves existing classes', t => {
44 | return process(t, 'preserves_classes');
45 | });
46 |
47 | test('Throws error when using an invalid language in class name', async t => {
48 | await t.throwsAsync(async () => {
49 | await process(t, 'invalid_language');
50 | }, {instanceOf: Error});
51 | });
52 |
--------------------------------------------------------------------------------
/xo.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | space: true,
3 | rules: {
4 | quotes: ['error', 'single', {allowTemplateLiterals: true}],
5 | 'promise/prefer-await-to-then': 0,
6 | 'unicorn/prefer-node-protocol': 0,
7 | 'unicorn/string-content': 0,
8 | 'unicorn/prefer-module': 0,
9 | 'arrow-body-style': 0,
10 | },
11 | };
12 |
--------------------------------------------------------------------------------