();
45 | const codeblockInfo: CodeblockInfo = {
46 | showLineNumbers: false,
47 | highlightLines: null,
48 | }
49 | let startLineNum: number;
50 |
51 | for (const {from, to} of view.visibleRanges) {
52 | try {
53 | const tree = syntaxTree(view.state)
54 |
55 | tree.iterate({
56 | from, to,
57 | // @ts-ignore
58 | enter: ({type, from, to}) => {
59 | const lineClasses = type.prop(lineClassNodeProp)
60 |
61 | if (!lineClasses) return ;
62 | const classes = new Set(lineClasses.split(' '));
63 | const isCodeblockBegin = classes.has('HyperMD-codeblock-begin');
64 | const isCodeblockLine =
65 | classes.has('HyperMD-codeblock-bg')
66 | && !classes.has('HyperMD-codeblock-begin')
67 | && !classes.has('HyperMD-codeblock-end');
68 |
69 | // reset data when found codeblock begin line.
70 | if (isCodeblockBegin) {
71 | const startLine = view.state.doc.lineAt(from);
72 | const codeblockParams = startLine.text.match(paramRegex).slice(1);
73 | const highlightParam = codeblockParams.find((param) => braceSurroundingRegex.test(param))?.slice(1, -1);
74 |
75 | startLineNum = startLine.number;
76 | codeblockInfo.showLineNumbers = false;
77 | codeblockInfo.highlightLines = null;
78 |
79 | if (codeblockParams.includes('nums')) codeblockInfo.showLineNumbers = true;
80 | if (highlightParam) codeblockInfo.highlightLines = highlightParam.replace(' ', '').split(',').flatMap((line) => {
81 | if (!+line) {
82 | const res = [];
83 | const [start, end] = line.split('-');
84 | for (let i = +start; i <= +end; i++) {
85 | res.push(i);
86 | }
87 |
88 | return res;
89 | }
90 |
91 | return [+line];
92 | });
93 | }
94 |
95 | if (!isCodeblockLine) return ;
96 |
97 | const currentLineNum = view.state.doc.lineAt(from).number;
98 |
99 | if (codeblockInfo.showLineNumbers) {
100 | const deco = Decoration.widget({
101 | widget: new LineNumberWidget(currentLineNum - startLineNum),
102 | side: -10000
103 | });
104 | builder.add(from, from, deco);
105 | }
106 |
107 | if (codeblockInfo.highlightLines) {
108 | if (codeblockInfo.highlightLines.includes(currentLineNum - startLineNum)) {
109 | const line = view.state.doc.lineAt(from);
110 | const deco = Decoration.line({
111 | attributes: {class: 'live-preview-codeblock-highlight'}
112 | })
113 |
114 | // @ts-ignore
115 | if (builder.last?.startSide) {
116 | // @ts-ignore
117 | deco.startSide = builder.last.startSide;
118 | deco.endSide = deco.startSide
119 | }
120 |
121 | builder.add(line.from, line.from, deco);
122 | }
123 | }
124 | }
125 | })
126 | } catch (error) {
127 | console.log(error)
128 | }
129 | }
130 |
131 | return builder.finish();
132 | }
133 | },
134 | {
135 | decorations: v => v.decorations
136 | })
--------------------------------------------------------------------------------
/lib/prism-line-numbers.js:
--------------------------------------------------------------------------------
1 | export function extendLineNumberPlugin (Prism) {
2 |
3 | if (typeof Prism === 'undefined' || typeof document === 'undefined') {
4 | return;
5 | }
6 |
7 | /**
8 | * Plugin name which is used as a class name for which is activating the plugin
9 | *
10 | * @type {string}
11 | */
12 | var PLUGIN_NAME = 'line-numbers';
13 |
14 | /**
15 | * Regular expression used for determining line breaks
16 | *
17 | * @type {RegExp}
18 | */
19 | var NEW_LINE_EXP = /\n(?!$)/g;
20 |
21 |
22 | /**
23 | * Global exports
24 | */
25 | var config = Prism.plugins.lineNumbers = {
26 | /**
27 | * Get node for provided line number
28 | *
29 | * @param {Element} element pre element
30 | * @param {number} number line number
31 | * @returns {Element|undefined}
32 | */
33 | getLine: function (element, number) {
34 | if (element.tagName !== 'PRE' || !element.classList.contains(PLUGIN_NAME)) {
35 | return;
36 | }
37 |
38 | var lineNumberRows = element.querySelector('.line-numbers-rows');
39 | if (!lineNumberRows) {
40 | return;
41 | }
42 | var lineNumberStart = parseInt(element.getAttribute('data-start'), 10) || 1;
43 | var lineNumberEnd = lineNumberStart + (lineNumberRows.children.length - 1);
44 |
45 | if (number < lineNumberStart) {
46 | number = lineNumberStart;
47 | }
48 | if (number > lineNumberEnd) {
49 | number = lineNumberEnd;
50 | }
51 |
52 | var lineIndex = number - lineNumberStart;
53 |
54 | return lineNumberRows.children[lineIndex];
55 | },
56 |
57 | /**
58 | * Resizes the line numbers of the given element.
59 | *
60 | * This function will not add line numbers. It will only resize existing ones.
61 | *
62 | * @param {HTMLElement} element A `` element with line numbers.
63 | * @returns {void}
64 | */
65 | resize: function (element) {
66 | resizeElements([element]);
67 | },
68 |
69 | /**
70 | * Whether the plugin can assume that the units font sizes and margins are not depended on the size of
71 | * the current viewport.
72 | *
73 | * Setting this to `true` will allow the plugin to do certain optimizations for better performance.
74 | *
75 | * Set this to `false` if you use any of the following CSS units: `vh`, `vw`, `vmin`, `vmax`.
76 | *
77 | * @type {boolean}
78 | */
79 | assumeViewportIndependence: true
80 | };
81 |
82 | /**
83 | * Resizes the given elements.
84 | *
85 | * @param {HTMLElement[]} elements
86 | */
87 | function resizeElements(elements) {
88 | elements = elements.filter(function (e) {
89 | var codeStyles = getStyles(e);
90 | var whiteSpace = codeStyles['white-space'];
91 | return whiteSpace === 'pre-wrap' || whiteSpace === 'pre-line';
92 | });
93 |
94 | if (elements.length == 0) {
95 | return;
96 | }
97 |
98 | var infos = elements.map(function (element) {
99 | var codeElement = element.querySelector('code');
100 | var lineNumbersWrapper = element.querySelector('.line-numbers-rows');
101 | if (!codeElement || !lineNumbersWrapper) {
102 | return undefined;
103 | }
104 |
105 | /** @type {HTMLElement} */
106 | var lineNumberSizer = element.querySelector('.line-numbers-sizer');
107 | var codeLines = codeElement.textContent.split(NEW_LINE_EXP);
108 |
109 | if (!lineNumberSizer) {
110 | lineNumberSizer = document.createElement('span');
111 | lineNumberSizer.className = 'line-numbers-sizer';
112 |
113 | codeElement.appendChild(lineNumberSizer);
114 | }
115 |
116 | lineNumberSizer.innerHTML = '0';
117 | lineNumberSizer.style.display = 'block';
118 |
119 | var oneLinerHeight = lineNumberSizer.getBoundingClientRect().height;
120 | lineNumberSizer.innerHTML = '';
121 |
122 | return {
123 | element: element,
124 | lines: codeLines,
125 | lineHeights: [],
126 | oneLinerHeight: oneLinerHeight,
127 | sizer: lineNumberSizer,
128 | };
129 | }).filter(Boolean);
130 |
131 | infos.forEach(function (info) {
132 | var lineNumberSizer = info.sizer;
133 | var lines = info.lines;
134 | var lineHeights = info.lineHeights;
135 | var oneLinerHeight = info.oneLinerHeight;
136 |
137 | lineHeights[lines.length - 1] = undefined;
138 | lines.forEach(function (line, index) {
139 | if (line && line.length > 1) {
140 | var e = lineNumberSizer.appendChild(document.createElement('span'));
141 | e.style.display = 'block';
142 | e.textContent = line;
143 | } else {
144 | lineHeights[index] = oneLinerHeight;
145 | }
146 | });
147 | });
148 |
149 | infos.forEach(function (info) {
150 | var lineNumberSizer = info.sizer;
151 | var lineHeights = info.lineHeights;
152 |
153 | var childIndex = 0;
154 | for (var i = 0; i < lineHeights.length; i++) {
155 | if (lineHeights[i] === undefined) {
156 | lineHeights[i] = lineNumberSizer.children[childIndex++].getBoundingClientRect().height;
157 | }
158 | }
159 | });
160 |
161 | infos.forEach(function (info) {
162 | var lineNumberSizer = info.sizer;
163 | var wrapper = info.element.querySelector('.line-numbers-rows');
164 |
165 | lineNumberSizer.style.display = 'none';
166 | lineNumberSizer.innerHTML = '';
167 |
168 | info.lineHeights.forEach(function (height, lineNumber) {
169 | wrapper.children[lineNumber].style.height = height + 'px';
170 | });
171 | });
172 | }
173 |
174 | /**
175 | * Returns style declarations for the element
176 | *
177 | * @param {Element} element
178 | */
179 | function getStyles(element) {
180 | if (!element) {
181 | return null;
182 | }
183 |
184 | return window.getComputedStyle ? getComputedStyle(element) : (element.currentStyle || null);
185 | }
186 |
187 | // var lastWidth = undefined;
188 | // window.addEventListener('resize', function () {
189 | // if (config.assumeViewportIndependence && lastWidth === window.innerWidth) {
190 | // return;
191 | // }
192 | // lastWidth = window.innerWidth;
193 |
194 | // resizeElements(Array.prototype.slice.call(document.querySelectorAll('pre.' + PLUGIN_NAME)));
195 | // });
196 |
197 | Prism.hooks.add('complete', function (env) {
198 | if (!env.code) {
199 | return;
200 | }
201 |
202 | var code = /** @type {Element} */ (env.element);
203 | var pre = /** @type {HTMLElement} */ (code.parentNode);
204 |
205 | // works only for wrapped inside (not inline)
206 | if (!pre || !/pre/i.test(pre.nodeName)) {
207 | return;
208 | }
209 |
210 | // Abort if line numbers already exists
211 | if (code.querySelector('.line-numbers-rows')) {
212 | return;
213 | }
214 |
215 | // only add line numbers if or one of its ancestors has the `line-numbers` class
216 | if (!Prism.util.isActive(code, PLUGIN_NAME)) {
217 | return;
218 | }
219 |
220 | // Remove the class 'line-numbers' from the
221 | code.classList.remove(PLUGIN_NAME);
222 | // Add the class 'line-numbers' to the
223 | pre.classList.add(PLUGIN_NAME);
224 |
225 | var match = env.code.match(NEW_LINE_EXP);
226 | var linesNum = match ? match.length + 1 : 1;
227 | var lineNumbersWrapper;
228 |
229 | var lines = new Array(linesNum + 1).join('');
230 |
231 | lineNumbersWrapper = document.createElement('span');
232 | lineNumbersWrapper.setAttribute('aria-hidden', 'true');
233 | lineNumbersWrapper.className = 'line-numbers-rows';
234 | lineNumbersWrapper.innerHTML = lines;
235 |
236 | if (pre.hasAttribute('data-start')) {
237 | pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1);
238 | }
239 |
240 | env.element.appendChild(lineNumbersWrapper);
241 |
242 | resizeElements([pre]);
243 |
244 | Prism.hooks.run('line-numbers', env);
245 | });
246 |
247 | Prism.hooks.add('line-numbers', function (env) {
248 | env.plugins = env.plugins || {};
249 | env.plugins.lineNumbers = true;
250 | });
251 |
252 | }
253 |
--------------------------------------------------------------------------------
/lib/prism-line-highlight.js:
--------------------------------------------------------------------------------
1 | export function extendLineHighlightPlugin (Prism) {
2 |
3 | if (typeof Prism === 'undefined' || typeof document === 'undefined' || !document.querySelector) {
4 | return;
5 | }
6 |
7 | var LINE_NUMBERS_CLASS = 'line-numbers';
8 | var LINKABLE_LINE_NUMBERS_CLASS = 'linkable-line-numbers';
9 | var NEW_LINE_REGEX = /\n(?!$)/g
10 |
11 | /**
12 | * @param {string} selector
13 | * @param {ParentNode} [container]
14 | * @returns {HTMLElement[]}
15 | */
16 | function $$(selector, container) {
17 | return Array.prototype.slice.call((container || document).querySelectorAll(selector));
18 | }
19 |
20 | /**
21 | * Returns whether the given element has the given class.
22 | *
23 | * @param {Element} element
24 | * @param {string} className
25 | * @returns {boolean}
26 | */
27 | function hasClass(element, className) {
28 | return element.classList.contains(className);
29 | }
30 |
31 | /**
32 | * Calls the given function.
33 | *
34 | * @param {() => any} func
35 | * @returns {void}
36 | */
37 | function callFunction(func) {
38 | func();
39 | }
40 |
41 | // Some browsers round the line-height, others don't.
42 | // We need to test for it to position the elements properly.
43 | var isLineHeightRounded = (function () {
44 | var res;
45 | return function () {
46 | if (typeof res === 'undefined') {
47 | var d = document.createElement('div');
48 | d.style.fontSize = '13px';
49 | d.style.lineHeight = '1.5';
50 | d.style.padding = '0';
51 | d.style.border = '0';
52 | d.innerHTML = '
';
53 | document.body.appendChild(d);
54 | // Browsers that round the line-height should have offsetHeight === 38
55 | // The others should have 39.
56 | res = d.offsetHeight === 38;
57 | document.body.removeChild(d);
58 | }
59 | return res;
60 | };
61 | }());
62 |
63 | /**
64 | * Returns the top offset of the content box of the given parent and the content box of one of its children.
65 | *
66 | * @param {HTMLElement} parent
67 | * @param {HTMLElement} child
68 | */
69 | function getContentBoxTopOffset(parent, child) {
70 | var parentStyle = getComputedStyle(parent);
71 | var childStyle = getComputedStyle(child);
72 |
73 | /**
74 | * Returns the numeric value of the given pixel value.
75 | *
76 | * @param {string} px
77 | */
78 | function pxToNumber(px) {
79 | return +px.substr(0, px.length - 2);
80 | }
81 |
82 | return child.offsetTop
83 | + pxToNumber(childStyle.borderTopWidth)
84 | + pxToNumber(childStyle.paddingTop)
85 | - pxToNumber(parentStyle.paddingTop);
86 | }
87 |
88 | /**
89 | * Returns whether the Line Highlight plugin is active for the given element.
90 | *
91 | * If this function returns `false`, do not call `highlightLines` for the given element.
92 | *
93 | * @param {HTMLElement | null | undefined} pre
94 | * @returns {boolean}
95 | */
96 | function isActiveFor(pre) {
97 | if (!pre || !/pre/i.test(pre.nodeName)) {
98 | return false;
99 | }
100 |
101 | if (pre.hasAttribute('data-line')) {
102 | return true;
103 | }
104 |
105 | if (pre.id && Prism.util.isActive(pre, LINKABLE_LINE_NUMBERS_CLASS)) {
106 | // Technically, the line numbers plugin is also necessary but this plugin doesn't control the classes of
107 | // the line numbers plugin, so we can't assume that they are present.
108 | return true;
109 | }
110 |
111 | return false;
112 | }
113 |
114 | var scrollIntoView = true;
115 |
116 | Prism.plugins.lineHighlight = {
117 | /**
118 | * Highlights the lines of the given pre.
119 | *
120 | * This function is split into a DOM measuring and mutate phase to improve performance.
121 | * The returned function mutates the DOM when called.
122 | *
123 | * @param {HTMLElement} pre
124 | * @param {string | null} [lines]
125 | * @param {string} [classes='']
126 | * @returns {() => void}
127 | */
128 | highlightLines: function highlightLines(pre, lines, classes) {
129 | lines = typeof lines === 'string' ? lines : (pre.getAttribute('data-line') || '');
130 |
131 | var ranges = lines.replace(/\s+/g, '').split(',').filter(Boolean);
132 | var offset = +pre.getAttribute('data-line-offset') || 0;
133 |
134 | var parseMethod = isLineHeightRounded() ? parseInt : parseFloat;
135 | var codeElement = pre.querySelector('code');
136 | var lineHeight = parseMethod(getComputedStyle(codeElement).lineHeight);
137 | var hasLineNumbers = Prism.util.isActive(pre, LINE_NUMBERS_CLASS);
138 | var parentElement = hasLineNumbers ? pre : codeElement || pre;
139 | var mutateActions = /** @type {(() => void)[]} */ ([]);
140 | var lineBreakMatch = codeElement.textContent.match(NEW_LINE_REGEX);
141 | var numberOfLines = lineBreakMatch? lineBreakMatch.length + 1: 1
142 |
143 | /**
144 | * The top offset between the content box of the element and the content box of the parent element of
145 | * the line highlight element (either `` or ``).
146 | *
147 | * This offset might not be zero for some themes where the element has a top margin. Some plugins
148 | * (or users) might also add element above the element. Because the line highlight is aligned relative
149 | * to the element, we have to take this into account.
150 | *
151 | * This offset will be 0 if the parent element of the line highlight element is the `` element.
152 | */
153 | var codePreOffset = !codeElement || parentElement == codeElement ? 0 : getContentBoxTopOffset(pre, codeElement);
154 |
155 | ranges.forEach(function (currentRange) {
156 | var range = currentRange.split('-');
157 |
158 | var start = +range[0];
159 | var end = +range[1] || start;
160 | end = Math.min(numberOfLines, end);
161 |
162 | if (end < start) return ;
163 |
164 | /** @type {HTMLElement} */
165 | var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div');
166 |
167 | mutateActions.push(function () {
168 | line.setAttribute('aria-hidden', 'true');
169 | line.setAttribute('data-range', currentRange);
170 | line.className = (classes || '') + ' line-highlight';
171 | });
172 |
173 | // if the line-numbers plugin is enabled, then there is no reason for this plugin to display the line numbers
174 | if (hasLineNumbers && Prism.plugins.lineNumbers) {
175 | var startNode = Prism.plugins.lineNumbers.getLine(pre, start);
176 | var endNode = Prism.plugins.lineNumbers.getLine(pre, end);
177 |
178 | if (startNode) {
179 | var top = startNode.offsetTop + codePreOffset + 'px';
180 | mutateActions.push(function () {
181 | line.style.top = top;
182 | });
183 | }
184 |
185 | if (endNode) {
186 | var height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px';
187 | mutateActions.push(function () {
188 | line.style.height = height;
189 | });
190 | }
191 |
192 | } else {
193 | mutateActions.push(function () {
194 | line.setAttribute('data-start', String(start));
195 |
196 | if (end > start) {
197 | line.setAttribute('data-end', String(end));
198 | }
199 |
200 | line.style.top = (start - offset - 1) * lineHeight + codePreOffset + 'px';
201 |
202 | line.textContent = new Array(end - start + 2).join(' \n');
203 | });
204 | }
205 |
206 | mutateActions.push(function () {
207 | line.style.width = pre.scrollWidth + 'px';
208 | });
209 |
210 | mutateActions.push(function () {
211 | // allow this to play nicely with the line-numbers plugin
212 | // need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning
213 | parentElement.appendChild(line);
214 | });
215 | });
216 |
217 | var id = pre.id;
218 | if (hasLineNumbers && Prism.util.isActive(pre, LINKABLE_LINE_NUMBERS_CLASS) && id) {
219 | // This implements linkable line numbers. Linkable line numbers use Line Highlight to create a link to a
220 | // specific line. For this to work, the pre element has to:
221 | // 1) have line numbers,
222 | // 2) have the `linkable-line-numbers` class or an ascendant that has that class, and
223 | // 3) have an id.
224 |
225 | if (!hasClass(pre, LINKABLE_LINE_NUMBERS_CLASS)) {
226 | // add class to pre
227 | mutateActions.push(function () {
228 | pre.classList.add(LINKABLE_LINE_NUMBERS_CLASS);
229 | });
230 | }
231 |
232 | var start = parseInt(pre.getAttribute('data-start') || '1');
233 |
234 | // iterate all line number spans
235 | $$('.line-numbers-rows > span', pre).forEach(function (lineSpan, i) {
236 | var lineNumber = i + start;
237 | lineSpan.onclick = function () {
238 | var hash = id + '.' + lineNumber;
239 |
240 | // this will prevent scrolling since the span is obviously in view
241 | scrollIntoView = false;
242 | location.hash = hash;
243 | setTimeout(function () {
244 | scrollIntoView = true;
245 | }, 1);
246 | };
247 | });
248 | }
249 |
250 | return function () {
251 | mutateActions.forEach(callFunction);
252 | };
253 | }
254 | };
255 |
256 |
257 | function applyHash() {
258 | var hash = location.hash.slice(1);
259 |
260 | // Remove pre-existing temporary lines
261 | $$('.temporary.line-highlight').forEach(function (line) {
262 | line.parentNode.removeChild(line);
263 | });
264 |
265 | var range = (hash.match(/\.([\d,-]+)$/) || [, ''])[1];
266 |
267 | if (!range || document.getElementById(hash)) {
268 | return;
269 | }
270 |
271 | var id = hash.slice(0, hash.lastIndexOf('.'));
272 | var pre = document.getElementById(id);
273 |
274 | if (!pre) {
275 | return;
276 | }
277 |
278 | if (!pre.hasAttribute('data-line')) {
279 | pre.setAttribute('data-line', '');
280 | }
281 |
282 | var mutateDom = Prism.plugins.lineHighlight.highlightLines(pre, range, 'temporary ');
283 | mutateDom();
284 |
285 | if (scrollIntoView) {
286 | document.querySelector('.temporary.line-highlight').scrollIntoView();
287 | }
288 | }
289 |
290 | var fakeTimer = 0; // Hack to limit the number of times applyHash() runs
291 |
292 | Prism.hooks.add('before-sanity-check', function (env) {
293 | var pre = env.element.parentElement;
294 | if (!isActiveFor(pre)) {
295 | return;
296 | }
297 |
298 | /*
299 | * Cleanup for other plugins (e.g. autoloader).
300 | *
301 | * Sometimes blocks are highlighted multiple times. It is necessary
302 | * to cleanup any left-over tags, because the whitespace inside of the
303 | * tags change the content of the tag.
304 | */
305 | var num = 0;
306 | $$('.line-highlight', pre).forEach(function (line) {
307 | num += line.textContent.length;
308 | line.parentNode.removeChild(line);
309 | });
310 | // Remove extra whitespace
311 | if (num && /^(?: \n)+$/.test(env.code.slice(-num))) {
312 | env.code = env.code.slice(0, -num);
313 | }
314 | });
315 |
316 | Prism.hooks.add('complete', function completeHook(env) {
317 | var pre = env.element.parentElement;
318 | if (!isActiveFor(pre)) {
319 | return;
320 | }
321 |
322 | clearTimeout(fakeTimer);
323 |
324 | var hasLineNumbers = Prism.plugins.lineNumbers;
325 | var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers;
326 |
327 | if (hasClass(pre, LINE_NUMBERS_CLASS) && hasLineNumbers && !isLineNumbersLoaded) {
328 | Prism.hooks.add('line-numbers', completeHook);
329 | } else {
330 | var mutateDom = Prism.plugins.lineHighlight.highlightLines(pre);
331 | mutateDom();
332 | fakeTimer = setTimeout(applyHash, 1);
333 | }
334 | });
335 |
336 | window.addEventListener('hashchange', applyHash);
337 | // window.addEventListener('resize', function () {
338 | // var actions = $$('pre')
339 | // .filter(isActiveFor)
340 | // .map(function (pre) {
341 | // return Prism.plugins.lineHighlight.highlightLines(pre);
342 | // });
343 | // actions.forEach(callFunction);
344 | // });
345 |
346 | }
347 |
--------------------------------------------------------------------------------