Skip to content

Commit 03f2023

Browse files
authored
chore(plugins) move useBR, tabReplace, and HTML merging into internal plugins (#2873)
- This begins the official deprecation of HTML merging and tabReplace. - `useBR` was previously deprecated.
1 parent 1846044 commit 03f2023

File tree

8 files changed

+262
-173
lines changed

8 files changed

+262
-173
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
"init-declarations": ["error", "always"],
2727
"array-callback-return": "error",
2828
"block-scoped-var": "error",
29+
"no-multiple-empty-lines": ["error", { max: 2 }],
2930
// we like our semi-colons
3031
semi: ["error", "always"],
3132
// our codebase doesn't do this at all, so disabled for now
@@ -57,7 +58,7 @@ module.exports = {
5758
// // languages are all over the map and we don't want to
5859
// // do a mass edit so turn off the most egregious rule violations
5960
// indent: "off",
60-
"camelcase": 0,
61+
camelcase: 0,
6162
"no-control-regex": 0,
6263
"no-useless-escape": 0,
6364
"comma-dangle": 1,

CHANGES.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,32 @@ Grammar improvements:
2929
- enh(dart) Fix empty doc-comment eating next line [Jan Pilzer][]
3030
- enh(asciidoc) Adds support for unconstrained bold syntax (#2869) [Guillaume Grossetie][]
3131

32+
Deprecations (*these will be mentioned in every release*):
33+
34+
- HTML "merging" is deprecated. (#2873) [Josh Goebel][]
35+
- HTML inside `<pre>` blocks will no longer be magically merged back into the
36+
highlighted code's HTML result - it will instead be silently removed.
37+
- Consider [using a plugin][htmlPlugin] if you truly need this functionality
38+
- Deprecated as of 10.5.0 - will be removed in v11.
39+
- `tabReplace` option deprecated. (#2873) [Josh Goebel][]
40+
- **Consider:** Use the CSS `tab-size` property, or simply pre-process the
41+
text yourself before rendering the initial HTML
42+
- otherwise, [use a plugin][tabPlugin]
43+
- Deprecated as of 10.5.0 - will be removed in v11.
44+
- `useBR` option deprecated. (#2559) [Josh Goebel][]
45+
- **Recommended:** You really should just use the HTML `<pre>` tag
46+
- or perhaps try CSS `white-space: pre;`
47+
- otherwise, [use a plugin][brPlugin]
48+
- Deprecated as of 10.3.0 - will be removed in v11.
49+
- `requireLanguage` API is deprecated, will be removed in v11.0.
50+
- **Consider:** Use `getLanguage` (with custom error handling) or built-time dependencies.
51+
- See [Library API](https://highlightjs.readthedocs.io/en/latest/api.html#requirelanguage-name) for more information.
52+
- Deprecated as of 10.4.0 - will be removed in v11.
53+
54+
[htmlPlugin]: https://github.com/highlightjs/highlight.js/issues/2889
55+
[tabPlugin]: https://github.com/highlightjs/highlight.js/issues/2874
56+
[brPlugin]: https://github.com/highlightjs/highlight.js/issues/2559
57+
3258
[Martin Dørum]: https://github.com/mortie
3359
[Jan Pilzer]: https://github.com/Hirse
3460
[Oldes Huhuman]: https://github.com/Oldes
@@ -101,6 +127,7 @@ Parser:
101127
- enh(grammars) allow `classNameAliases` for more complex grammars [Josh Goebel][]
102128
- fix(vue): Language name now appears in CSS class (#2807) [Michael Rush][]
103129
- (chore) Clean up all regexs to be UTF-8 compliant/ready (#2759) [Josh Goebel][]
130+
- enh(grammars) allow `classNameAliases` for more complex grammars [Josh Goebel][]
104131

105132
New Languages:
106133

README.md

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ Highlight.js supports over 180 different languages in the core library. There a
8787
language plugins available for additional languages. You can find the full list of supported languages
8888
in [SUPPORTED_LANGUAGES.md][9].
8989

90-
## Custom Initialization
90+
## Custom Scenarios
9191

9292
When you need a bit more control over the initialization of
9393
highlight.js, you can use the [`highlightBlock`][3] and [`configure`][4]
94-
functions. This allows you to control *what* to highlight and *when*.
94+
functions. This allows you to better control *what* to highlight and *when*.
9595

96-
Here’s an equivalent way to calling [`initHighlightingOnLoad`][1] using
97-
vanilla JS:
96+
Here’s the equivalent of calling [`initHighlightingOnLoad`][1] using
97+
only vanilla JS:
9898

9999
```js
100100
document.addEventListener('DOMContentLoaded', (event) => {
@@ -104,19 +104,45 @@ document.addEventListener('DOMContentLoaded', (event) => {
104104
});
105105
```
106106

107-
You can use any tags instead of `<pre><code>` to mark up your code. If
108-
you don't use a container that preserves line breaks you will need to
109-
configure highlight.js to use the `<br>` tag:
107+
Please refer to the documentation for [`configure`][4] options.
110108

111-
```js
112-
hljs.configure({useBR: true});
113109

114-
document.querySelectorAll('div.code').forEach((block) => {
110+
### Using custom HTML elements for code blocks
111+
112+
We strongly recommend `<pre><code>` wrapping for code blocks. It's quite
113+
semantic and "just works" out of the box with zero fiddling. It is possible to
114+
use other HTML elements (or combos), but you may need to pay special attention to
115+
preserving linebreaks.
116+
117+
Let's say your markup for code blocks uses divs:
118+
119+
```html
120+
<div class='code'>...</div>
121+
```
122+
123+
To highlight such blocks manually:
124+
125+
```js
126+
// first, find all the div.code blocks
127+
document.querySelectorAll('div.code').forEach(block => {
128+
// then highlight each
115129
hljs.highlightBlock(block);
116130
});
117131
```
118132

119-
For other options refer to the documentation for [`configure`][4].
133+
Without using a tag that preserves linebreaks (like `pre`) you'll need some
134+
additional CSS to help preserve them. You could also [pre and post-process line
135+
breaks with a plug-in][brPlugin], but *we recommend using CSS*.
136+
137+
[brPlugin]: https://github.com/highlightjs/highlight.js/issues/2559
138+
139+
To preserve linebreaks inside a `div` using CSS:
140+
141+
```css
142+
div.code {
143+
white-space: pre;
144+
}
145+
```
120146

121147

122148
## Using with Vue.js

docs/plugin-api.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ Note: This callback does not fire from highlighting resulting from auto-language
101101
It returns nothing.
102102

103103

104-
after:highlightBlock({block, result})
105-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
104+
after:highlightBlock({block, result, text})
105+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
106106

107107
This callback function is passed an object with two keys:
108108

@@ -112,11 +112,14 @@ block
112112
result
113113
The result object returned by `highlight` or `highlightAuto`.
114114

115+
text
116+
The raw text that was to be highlighted.
117+
115118
It returns nothing.
116119

117120

118121
before:highlightBlock({block, language})
119-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
122+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
120123

121124
This callback function is passed an object with two keys:
122125

src/highlight.js

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ import * as MODES from './lib/modes.js';
1313
import { compileLanguage } from './lib/mode_compiler.js';
1414
import * as packageJSON from '../package.json';
1515
import { BuildVuePlugin } from "./plugins/vue.js";
16+
import { mergeHTMLPlugin } from "./plugins/merge_html.js";
1617

1718
const escape = utils.escapeHTML;
1819
const inherit = utils.inherit;
19-
20-
const { nodeStream, mergeStreams } = utils;
2120
const NO_MATCH = Symbol("nomatch");
2221

2322
/**
@@ -667,6 +666,32 @@ const HLJS = function(hljs) {
667666
return result.join(' ').trim();
668667
}
669668

669+
/** @type {HLJSPlugin} */
670+
const brPlugin = {
671+
"before:highlightBlock": ({ block }) => {
672+
if (options.useBR) {
673+
block.innerHTML = block.innerHTML.replace(/\n/g, '').replace(/<br[ /]*>/g, '\n');
674+
}
675+
},
676+
"after:highlightBlock": ({ result }) => {
677+
if (options.useBR) {
678+
result.value = result.value.replace(/\n/g, "<br>");
679+
}
680+
}
681+
};
682+
683+
const TAB_REPLACE_RE = /^(<[^>]+>|\t)+/gm;
684+
/** @type {HLJSPlugin} */
685+
const tabReplacePlugin = {
686+
"after:highlightBlock": ({ result }) => {
687+
if (options.tabReplace) {
688+
result.value = result.value.replace(TAB_REPLACE_RE, (m) =>
689+
m.replace(/\t/g, options.tabReplace)
690+
);
691+
}
692+
}
693+
};
694+
670695
/**
671696
* Applies highlighting to a DOM node containing code. Accepts a DOM node and
672697
* two optional parameters for fixMarkup.
@@ -683,24 +708,11 @@ const HLJS = function(hljs) {
683708
fire("before:highlightBlock",
684709
{ block: element, language: language });
685710

686-
if (options.useBR) {
687-
node = document.createElement('div');
688-
node.innerHTML = element.innerHTML.replace(/\n/g, '').replace(/<br[ /]*>/g, '\n');
689-
} else {
690-
node = element;
691-
}
711+
node = element;
692712
const text = node.textContent;
693713
const result = language ? highlight(language, text, true) : highlightAuto(text);
694714

695-
const originalStream = nodeStream(node);
696-
if (originalStream.length) {
697-
const resultNode = document.createElement('div');
698-
resultNode.innerHTML = result.value;
699-
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
700-
}
701-
result.value = fixMarkup(result.value);
702-
703-
fire("after:highlightBlock", { block: element, result: result });
715+
fire("after:highlightBlock", { block: element, result, text });
704716

705717
element.innerHTML = result.value;
706718
element.className = buildClassName(element.className, language, result.language);
@@ -909,6 +921,10 @@ const HLJS = function(hljs) {
909921
// merge all the modes/regexs into our main object
910922
Object.assign(hljs, MODES);
911923

924+
// built-in plugins, likely to be moved out of core in the future
925+
hljs.addPlugin(brPlugin); // slated to be removed in v11
926+
hljs.addPlugin(mergeHTMLPlugin);
927+
hljs.addPlugin(tabReplacePlugin);
912928
return hljs;
913929
};
914930

src/lib/utils.js

Lines changed: 0 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -34,143 +34,3 @@ export function inherit(original, ...objects) {
3434
return /** @type {T} */ (result);
3535
}
3636

37-
/* Stream merging */
38-
39-
/**
40-
* @typedef Event
41-
* @property {'start'|'stop'} event
42-
* @property {number} offset
43-
* @property {Node} node
44-
*/
45-
46-
/**
47-
* @param {Node} node
48-
*/
49-
function tag(node) {
50-
return node.nodeName.toLowerCase();
51-
}
52-
53-
/**
54-
* @param {Node} node
55-
*/
56-
export function nodeStream(node) {
57-
/** @type Event[] */
58-
const result = [];
59-
(function _nodeStream(node, offset) {
60-
for (let child = node.firstChild; child; child = child.nextSibling) {
61-
if (child.nodeType === 3) {
62-
offset += child.nodeValue.length;
63-
} else if (child.nodeType === 1) {
64-
result.push({
65-
event: 'start',
66-
offset: offset,
67-
node: child
68-
});
69-
offset = _nodeStream(child, offset);
70-
// Prevent void elements from having an end tag that would actually
71-
// double them in the output. There are more void elements in HTML
72-
// but we list only those realistically expected in code display.
73-
if (!tag(child).match(/br|hr|img|input/)) {
74-
result.push({
75-
event: 'stop',
76-
offset: offset,
77-
node: child
78-
});
79-
}
80-
}
81-
}
82-
return offset;
83-
})(node, 0);
84-
return result;
85-
}
86-
87-
/**
88-
* @param {any} original - the original stream
89-
* @param {any} highlighted - stream of the highlighted source
90-
* @param {string} value - the original source itself
91-
*/
92-
export function mergeStreams(original, highlighted, value) {
93-
let processed = 0;
94-
let result = '';
95-
const nodeStack = [];
96-
97-
function selectStream() {
98-
if (!original.length || !highlighted.length) {
99-
return original.length ? original : highlighted;
100-
}
101-
if (original[0].offset !== highlighted[0].offset) {
102-
return (original[0].offset < highlighted[0].offset) ? original : highlighted;
103-
}
104-
105-
/*
106-
To avoid starting the stream just before it should stop the order is
107-
ensured that original always starts first and closes last:
108-
109-
if (event1 == 'start' && event2 == 'start')
110-
return original;
111-
if (event1 == 'start' && event2 == 'stop')
112-
return highlighted;
113-
if (event1 == 'stop' && event2 == 'start')
114-
return original;
115-
if (event1 == 'stop' && event2 == 'stop')
116-
return highlighted;
117-
118-
... which is collapsed to:
119-
*/
120-
return highlighted[0].event === 'start' ? original : highlighted;
121-
}
122-
123-
/**
124-
* @param {Node} node
125-
*/
126-
function open(node) {
127-
/** @param {Attr} attr */
128-
function attributeString(attr) {
129-
return ' ' + attr.nodeName + '="' + escapeHTML(attr.value) + '"';
130-
}
131-
// @ts-ignore
132-
result += '<' + tag(node) + [].map.call(node.attributes, attributeString).join('') + '>';
133-
}
134-
135-
/**
136-
* @param {Node} node
137-
*/
138-
function close(node) {
139-
result += '</' + tag(node) + '>';
140-
}
141-
142-
/**
143-
* @param {Event} event
144-
*/
145-
function render(event) {
146-
(event.event === 'start' ? open : close)(event.node);
147-
}
148-
149-
while (original.length || highlighted.length) {
150-
let stream = selectStream();
151-
result += escapeHTML(value.substring(processed, stream[0].offset));
152-
processed = stream[0].offset;
153-
if (stream === original) {
154-
/*
155-
On any opening or closing tag of the original markup we first close
156-
the entire highlighted node stack, then render the original tag along
157-
with all the following original tags at the same offset and then
158-
reopen all the tags on the highlighted stack.
159-
*/
160-
nodeStack.reverse().forEach(close);
161-
do {
162-
render(stream.splice(0, 1)[0]);
163-
stream = selectStream();
164-
} while (stream === original && stream.length && stream[0].offset === processed);
165-
nodeStack.reverse().forEach(open);
166-
} else {
167-
if (stream[0].event === 'start') {
168-
nodeStack.push(stream[0].node);
169-
} else {
170-
nodeStack.pop();
171-
}
172-
render(stream.splice(0, 1)[0]);
173-
}
174-
}
175-
return result + escapeHTML(value.substr(processed));
176-
}

0 commit comments

Comments
 (0)