ep_hljs 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/.github/workflows/automerge.yml +45 -0
  3. package/.github/workflows/backend-tests.yml +75 -0
  4. package/.github/workflows/codeql.yml +41 -0
  5. package/.github/workflows/frontend-tests.yml +72 -0
  6. package/.github/workflows/npmpublish.yml +124 -0
  7. package/.github/workflows/test-and-release.yml +32 -0
  8. package/CLAUDE.md +141 -0
  9. package/LICENSE +201 -0
  10. package/README.md +56 -0
  11. package/demo.gif +0 -0
  12. package/demo.png +0 -0
  13. package/ep.json +26 -0
  14. package/index.js +109 -0
  15. package/lib/exportRenderer.js +39 -0
  16. package/lib/languageAllowlist.js +16 -0
  17. package/lib/padLanguageStore.js +27 -0
  18. package/locales/en.json +10 -0
  19. package/package.json +59 -0
  20. package/pnpm-workspace.yaml +2 -0
  21. package/scripts/build-vendor.js +45 -0
  22. package/static/css/editor.css +94 -0
  23. package/static/css/themes/github-dark.css +118 -0
  24. package/static/css/themes/github.css +118 -0
  25. package/static/js/codeIndent.js +107 -0
  26. package/static/js/constants.js +7 -0
  27. package/static/js/domOverlay.js +16 -0
  28. package/static/js/highlightRegistry.js +109 -0
  29. package/static/js/hljsAdapter.js +80 -0
  30. package/static/js/index.js +124 -0
  31. package/static/js/lruCache.js +28 -0
  32. package/static/js/syntaxRenderer.js +201 -0
  33. package/static/js/themeBridge.js +76 -0
  34. package/static/js/vendor/hljs.min.js +5 -0
  35. package/static/tests/backend/specs/codeIndent.test.js +144 -0
  36. package/static/tests/backend/specs/export.test.js +47 -0
  37. package/static/tests/backend/specs/highlightRegistry.test.js +59 -0
  38. package/static/tests/backend/specs/hljsAdapter.test.js +43 -0
  39. package/static/tests/backend/specs/lruCache.test.js +45 -0
  40. package/static/tests/backend/specs/padLanguageStore.test.js +63 -0
  41. package/static/tests/backend/specs/socket.test.js +54 -0
  42. package/static/tests/frontend-new/helper/highlights.ts +64 -0
  43. package/static/tests/frontend-new/specs/caret-stability.spec.ts +106 -0
  44. package/static/tests/frontend-new/specs/code-indent.spec.ts +78 -0
  45. package/static/tests/frontend-new/specs/collaboration.spec.ts +59 -0
  46. package/static/tests/frontend-new/specs/content-sync.spec.ts +45 -0
  47. package/static/tests/frontend-new/specs/dark-mode.spec.ts +87 -0
  48. package/static/tests/frontend-new/specs/export.spec.ts +31 -0
  49. package/static/tests/frontend-new/specs/initial-paint.spec.ts +49 -0
  50. package/static/tests/frontend-new/specs/language-picker.spec.ts +54 -0
  51. package/static/tests/frontend-new/specs/large-pad.spec.ts +36 -0
  52. package/static/tests/frontend-new/specs/lifecycle.spec.ts +27 -0
  53. package/static/tests/frontend-new/specs/multi-user-caret.spec.ts +167 -0
  54. package/static/tests/frontend-new/specs/single-line-while.spec.ts +50 -0
  55. package/templates/editbarButtons.ejs +29 -0
@@ -0,0 +1,94 @@
1
+ /* CSS Custom Highlights — paint syntax tokens by Range registration in
2
+ highlightRegistry.js. Light palette tuned for visibility against the
3
+ default white-ish editor background; dark palette below mirrors GitHub
4
+ dark and is scoped via the super-dark-editor skin variant Etherpad
5
+ sets on the inner iframe's <html>. */
6
+ ::highlight(hljs-keyword) { color: #d73a49; font-weight: bold; }
7
+ ::highlight(hljs-doctag) { color: #d73a49; }
8
+ ::highlight(hljs-template-tag) { color: #d73a49; }
9
+ ::highlight(hljs-template-variable) { color: #d73a49; }
10
+ ::highlight(hljs-type) { color: #d73a49; }
11
+ ::highlight(hljs-title) { color: #6f42c1; }
12
+ ::highlight(hljs-attr) { color: #3b82f6; }
13
+ ::highlight(hljs-attribute) { color: #3b82f6; }
14
+ ::highlight(hljs-literal) { color: #3b82f6; }
15
+ ::highlight(hljs-meta) { color: #3b82f6; }
16
+ ::highlight(hljs-number) { color: #3b82f6; }
17
+ ::highlight(hljs-operator) { color: #3b82f6; }
18
+ ::highlight(hljs-variable) { color: #3b82f6; }
19
+ ::highlight(hljs-selector-attr) { color: #3b82f6; }
20
+ ::highlight(hljs-selector-class) { color: #3b82f6; }
21
+ ::highlight(hljs-selector-id) { color: #3b82f6; }
22
+ ::highlight(hljs-regexp) { color: #1d4ed8; }
23
+ ::highlight(hljs-string) { color: #1d4ed8; }
24
+ ::highlight(hljs-built_in) { color: #e36209; }
25
+ ::highlight(hljs-symbol) { color: #e36209; }
26
+ ::highlight(hljs-comment) { color: #6a737d; font-style: italic; }
27
+ ::highlight(hljs-code) { color: #6a737d; }
28
+ ::highlight(hljs-formula) { color: #6a737d; }
29
+ ::highlight(hljs-name) { color: #22863a; }
30
+ ::highlight(hljs-quote) { color: #22863a; }
31
+ ::highlight(hljs-selector-tag) { color: #22863a; }
32
+ ::highlight(hljs-selector-pseudo) { color: #22863a; }
33
+ ::highlight(hljs-tag) { color: #22863a; }
34
+ ::highlight(hljs-section) { color: #3b82f6; font-weight: bold; }
35
+ ::highlight(hljs-bullet) { color: #735c0f; }
36
+ ::highlight(hljs-link) { color: #1d4ed8; text-decoration: underline; }
37
+ ::highlight(hljs-emphasis) { font-style: italic; }
38
+ ::highlight(hljs-strong) { font-weight: bold; }
39
+ ::highlight(hljs-addition) { color: #22863a; background: #f0fff4; }
40
+ ::highlight(hljs-deletion) { color: #b31d28; background: #ffeef0; }
41
+
42
+ /* Authorship background colors fight syntax foreground colors and make
43
+ tokens hard to read. Suppress them only when this plugin is *actively*
44
+ highlighting (state.language is a real grammar AND user has highlighting
45
+ enabled). Plain pads — and pads where this plugin loaded but is not
46
+ currently painting — keep their author backgrounds untouched, so core
47
+ Etherpad tests for authorship rendering pass unaffected. */
48
+ .ep-syntax-highlighting-active [class*="author-a"] {
49
+ background-color: transparent !important;
50
+ }
51
+
52
+ #ep_hljs_paused_badge {
53
+ margin-left: 6px;
54
+ font-style: italic;
55
+ opacity: 0.7;
56
+ }
57
+
58
+ /* Dark mode (Etherpad's super-dark-editor skin variant). Palette taken
59
+ from highlight.js's github-dark.css. */
60
+ .super-dark-editor ::highlight(hljs-keyword) { color: #ff7b72; font-weight: bold; }
61
+ .super-dark-editor ::highlight(hljs-doctag) { color: #ff7b72; }
62
+ .super-dark-editor ::highlight(hljs-template-tag) { color: #ff7b72; }
63
+ .super-dark-editor ::highlight(hljs-template-variable) { color: #ff7b72; }
64
+ .super-dark-editor ::highlight(hljs-type) { color: #ff7b72; }
65
+ .super-dark-editor ::highlight(hljs-title) { color: #d2a8ff; }
66
+ .super-dark-editor ::highlight(hljs-attr) { color: #79c0ff; }
67
+ .super-dark-editor ::highlight(hljs-attribute) { color: #79c0ff; }
68
+ .super-dark-editor ::highlight(hljs-literal) { color: #79c0ff; }
69
+ .super-dark-editor ::highlight(hljs-meta) { color: #79c0ff; }
70
+ .super-dark-editor ::highlight(hljs-number) { color: #79c0ff; }
71
+ .super-dark-editor ::highlight(hljs-operator) { color: #79c0ff; }
72
+ .super-dark-editor ::highlight(hljs-variable) { color: #79c0ff; }
73
+ .super-dark-editor ::highlight(hljs-selector-attr) { color: #79c0ff; }
74
+ .super-dark-editor ::highlight(hljs-selector-class) { color: #79c0ff; }
75
+ .super-dark-editor ::highlight(hljs-selector-id) { color: #79c0ff; }
76
+ .super-dark-editor ::highlight(hljs-regexp) { color: #a5d6ff; }
77
+ .super-dark-editor ::highlight(hljs-string) { color: #a5d6ff; }
78
+ .super-dark-editor ::highlight(hljs-built_in) { color: #ffa657; }
79
+ .super-dark-editor ::highlight(hljs-symbol) { color: #ffa657; }
80
+ .super-dark-editor ::highlight(hljs-comment) { color: #8b949e; font-style: italic; }
81
+ .super-dark-editor ::highlight(hljs-code) { color: #8b949e; }
82
+ .super-dark-editor ::highlight(hljs-formula) { color: #8b949e; }
83
+ .super-dark-editor ::highlight(hljs-name) { color: #7ee787; }
84
+ .super-dark-editor ::highlight(hljs-quote) { color: #7ee787; }
85
+ .super-dark-editor ::highlight(hljs-selector-tag) { color: #7ee787; }
86
+ .super-dark-editor ::highlight(hljs-selector-pseudo) { color: #7ee787; }
87
+ .super-dark-editor ::highlight(hljs-tag) { color: #7ee787; }
88
+ .super-dark-editor ::highlight(hljs-section) { color: #1f6feb; font-weight: bold; }
89
+ .super-dark-editor ::highlight(hljs-bullet) { color: #f2cc60; }
90
+ .super-dark-editor ::highlight(hljs-link) { color: #a5d6ff; text-decoration: underline; }
91
+ .super-dark-editor ::highlight(hljs-emphasis) { font-style: italic; }
92
+ .super-dark-editor ::highlight(hljs-strong) { font-weight: bold; }
93
+ .super-dark-editor ::highlight(hljs-addition) { color: #aff5b4; background: #033a16; }
94
+ .super-dark-editor ::highlight(hljs-deletion) { color: #ffdcd7; background: #67060c; }
@@ -0,0 +1,118 @@
1
+ pre code.hljs {
2
+ display: block;
3
+ overflow-x: auto;
4
+ padding: 1em
5
+ }
6
+ code.hljs {
7
+ padding: 3px 5px
8
+ }
9
+ /*!
10
+ Theme: GitHub Dark
11
+ Description: Dark theme as seen on github.com
12
+ Author: github.com
13
+ Maintainer: @Hirse
14
+ Updated: 2021-05-15
15
+
16
+ Outdated base version: https://github.com/primer/github-syntax-dark
17
+ Current colors taken from GitHub's CSS
18
+ */
19
+ .hljs {
20
+ color: #c9d1d9;
21
+ background: #0d1117
22
+ }
23
+ .hljs-doctag,
24
+ .hljs-keyword,
25
+ .hljs-meta .hljs-keyword,
26
+ .hljs-template-tag,
27
+ .hljs-template-variable,
28
+ .hljs-type,
29
+ .hljs-variable.language_ {
30
+ /* prettylights-syntax-keyword */
31
+ color: #ff7b72
32
+ }
33
+ .hljs-title,
34
+ .hljs-title.class_,
35
+ .hljs-title.class_.inherited__,
36
+ .hljs-title.function_ {
37
+ /* prettylights-syntax-entity */
38
+ color: #d2a8ff
39
+ }
40
+ .hljs-attr,
41
+ .hljs-attribute,
42
+ .hljs-literal,
43
+ .hljs-meta,
44
+ .hljs-number,
45
+ .hljs-operator,
46
+ .hljs-variable,
47
+ .hljs-selector-attr,
48
+ .hljs-selector-class,
49
+ .hljs-selector-id {
50
+ /* prettylights-syntax-constant */
51
+ color: #79c0ff
52
+ }
53
+ .hljs-regexp,
54
+ .hljs-string,
55
+ .hljs-meta .hljs-string {
56
+ /* prettylights-syntax-string */
57
+ color: #a5d6ff
58
+ }
59
+ .hljs-built_in,
60
+ .hljs-symbol {
61
+ /* prettylights-syntax-variable */
62
+ color: #ffa657
63
+ }
64
+ .hljs-comment,
65
+ .hljs-code,
66
+ .hljs-formula {
67
+ /* prettylights-syntax-comment */
68
+ color: #8b949e
69
+ }
70
+ .hljs-name,
71
+ .hljs-quote,
72
+ .hljs-selector-tag,
73
+ .hljs-selector-pseudo {
74
+ /* prettylights-syntax-entity-tag */
75
+ color: #7ee787
76
+ }
77
+ .hljs-subst {
78
+ /* prettylights-syntax-storage-modifier-import */
79
+ color: #c9d1d9
80
+ }
81
+ .hljs-section {
82
+ /* prettylights-syntax-markup-heading */
83
+ color: #1f6feb;
84
+ font-weight: bold
85
+ }
86
+ .hljs-bullet {
87
+ /* prettylights-syntax-markup-list */
88
+ color: #f2cc60
89
+ }
90
+ .hljs-emphasis {
91
+ /* prettylights-syntax-markup-italic */
92
+ color: #c9d1d9;
93
+ font-style: italic
94
+ }
95
+ .hljs-strong {
96
+ /* prettylights-syntax-markup-bold */
97
+ color: #c9d1d9;
98
+ font-weight: bold
99
+ }
100
+ .hljs-addition {
101
+ /* prettylights-syntax-markup-inserted */
102
+ color: #aff5b4;
103
+ background-color: #033a16
104
+ }
105
+ .hljs-deletion {
106
+ /* prettylights-syntax-markup-deleted */
107
+ color: #ffdcd7;
108
+ background-color: #67060c
109
+ }
110
+ .hljs-char.escape_,
111
+ .hljs-link,
112
+ .hljs-params,
113
+ .hljs-property,
114
+ .hljs-punctuation,
115
+ .hljs-tag {
116
+ /* purposely ignored */
117
+
118
+ }
@@ -0,0 +1,118 @@
1
+ pre code.hljs {
2
+ display: block;
3
+ overflow-x: auto;
4
+ padding: 1em
5
+ }
6
+ code.hljs {
7
+ padding: 3px 5px
8
+ }
9
+ /*!
10
+ Theme: GitHub
11
+ Description: Light theme as seen on github.com
12
+ Author: github.com
13
+ Maintainer: @Hirse
14
+ Updated: 2021-05-15
15
+
16
+ Outdated base version: https://github.com/primer/github-syntax-light
17
+ Current colors taken from GitHub's CSS
18
+ */
19
+ .hljs {
20
+ color: #24292e;
21
+ background: #ffffff
22
+ }
23
+ .hljs-doctag,
24
+ .hljs-keyword,
25
+ .hljs-meta .hljs-keyword,
26
+ .hljs-template-tag,
27
+ .hljs-template-variable,
28
+ .hljs-type,
29
+ .hljs-variable.language_ {
30
+ /* prettylights-syntax-keyword */
31
+ color: #d73a49
32
+ }
33
+ .hljs-title,
34
+ .hljs-title.class_,
35
+ .hljs-title.class_.inherited__,
36
+ .hljs-title.function_ {
37
+ /* prettylights-syntax-entity */
38
+ color: #6f42c1
39
+ }
40
+ .hljs-attr,
41
+ .hljs-attribute,
42
+ .hljs-literal,
43
+ .hljs-meta,
44
+ .hljs-number,
45
+ .hljs-operator,
46
+ .hljs-variable,
47
+ .hljs-selector-attr,
48
+ .hljs-selector-class,
49
+ .hljs-selector-id {
50
+ /* prettylights-syntax-constant */
51
+ color: #005cc5
52
+ }
53
+ .hljs-regexp,
54
+ .hljs-string,
55
+ .hljs-meta .hljs-string {
56
+ /* prettylights-syntax-string */
57
+ color: #032f62
58
+ }
59
+ .hljs-built_in,
60
+ .hljs-symbol {
61
+ /* prettylights-syntax-variable */
62
+ color: #e36209
63
+ }
64
+ .hljs-comment,
65
+ .hljs-code,
66
+ .hljs-formula {
67
+ /* prettylights-syntax-comment */
68
+ color: #6a737d
69
+ }
70
+ .hljs-name,
71
+ .hljs-quote,
72
+ .hljs-selector-tag,
73
+ .hljs-selector-pseudo {
74
+ /* prettylights-syntax-entity-tag */
75
+ color: #22863a
76
+ }
77
+ .hljs-subst {
78
+ /* prettylights-syntax-storage-modifier-import */
79
+ color: #24292e
80
+ }
81
+ .hljs-section {
82
+ /* prettylights-syntax-markup-heading */
83
+ color: #005cc5;
84
+ font-weight: bold
85
+ }
86
+ .hljs-bullet {
87
+ /* prettylights-syntax-markup-list */
88
+ color: #735c0f
89
+ }
90
+ .hljs-emphasis {
91
+ /* prettylights-syntax-markup-italic */
92
+ color: #24292e;
93
+ font-style: italic
94
+ }
95
+ .hljs-strong {
96
+ /* prettylights-syntax-markup-bold */
97
+ color: #24292e;
98
+ font-weight: bold
99
+ }
100
+ .hljs-addition {
101
+ /* prettylights-syntax-markup-inserted */
102
+ color: #22863a;
103
+ background-color: #f0fff4
104
+ }
105
+ .hljs-deletion {
106
+ /* prettylights-syntax-markup-deleted */
107
+ color: #b31d28;
108
+ background-color: #ffeef0
109
+ }
110
+ .hljs-char.escape_,
111
+ .hljs-link,
112
+ .hljs-params,
113
+ .hljs-property,
114
+ .hljs-punctuation,
115
+ .hljs-tag {
116
+ /* purposely ignored */
117
+
118
+ }
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ let indentSize = 2;
4
+ let getLanguage = () => null;
5
+ let getAutoDetect = () => true;
6
+
7
+ exports.start = (opts) => {
8
+ if (opts && typeof opts.indentSize === 'number' && opts.indentSize > 0) {
9
+ indentSize = opts.indentSize;
10
+ }
11
+ if (opts && typeof opts.getLanguage === 'function') {
12
+ getLanguage = opts.getLanguage;
13
+ }
14
+ if (opts && typeof opts.getAutoDetect === 'function') {
15
+ getAutoDetect = opts.getAutoDetect;
16
+ }
17
+ };
18
+
19
+ exports.setIndentSize = (n) => {
20
+ if (typeof n === 'number' && n > 0) indentSize = n;
21
+ };
22
+
23
+ exports.getIndentSize = () => indentSize;
24
+
25
+ const inCodeMode = () => {
26
+ const lang = getLanguage();
27
+ if (!lang || lang === 'auto' || lang === 'off') return false;
28
+ // Only intercept Tab / Enter / Shift+Tab when the user has *explicitly*
29
+ // picked a language. Auto-detect picking a language under the hood does
30
+ // not enable keystroke interception — otherwise core Etherpad tests that
31
+ // paste code-shaped text into a plain pad (e.g. indentation.spec.ts)
32
+ // would have their Tab/Enter overridden when auto-detect kicks in.
33
+ return getAutoDetect() === false;
34
+ };
35
+
36
+ const indentStr = (n) => ' '.repeat(n);
37
+
38
+ const leadingIndent = (text) => {
39
+ const m = /^([ \t]*)/.exec(text);
40
+ return m ? m[1] : '';
41
+ };
42
+
43
+ const handleEnter = (rep, editorInfo, evt) => {
44
+ const line = rep.selStart[0];
45
+ const col = rep.selStart[1];
46
+ const lineEntry = rep.lines.atIndex(line);
47
+ const text = (lineEntry && lineEntry.text) || '';
48
+ const beforeCaret = text.slice(0, col);
49
+ let indent = leadingIndent(text);
50
+ if (/[{[(]\s*$/.test(beforeCaret)) indent += indentStr(indentSize);
51
+ editorInfo.ace_replaceRange(rep.selStart, rep.selEnd, `\n${indent}`);
52
+ evt.preventDefault();
53
+ return true;
54
+ };
55
+
56
+ const handleTab = (rep, editorInfo, evt) => {
57
+ const a = rep.selStart;
58
+ const b = rep.selEnd;
59
+ if (a[0] === b[0]) {
60
+ editorInfo.ace_replaceRange(a, b, indentStr(indentSize));
61
+ } else {
62
+ const startLine = Math.min(a[0], b[0]);
63
+ const endLine = Math.max(a[0], b[0]);
64
+ for (let i = startLine; i <= endLine; i++) {
65
+ editorInfo.ace_replaceRange([i, 0], [i, 0], indentStr(indentSize));
66
+ }
67
+ }
68
+ evt.preventDefault();
69
+ return true;
70
+ };
71
+
72
+ const handleShiftTab = (rep, editorInfo, evt) => {
73
+ const a = rep.selStart;
74
+ const b = rep.selEnd;
75
+ const startLine = Math.min(a[0], b[0]);
76
+ const endLine = Math.max(a[0], b[0]);
77
+ let didAnything = false;
78
+ for (let i = startLine; i <= endLine; i++) {
79
+ const lineEntry = rep.lines.atIndex(i);
80
+ const text = (lineEntry && lineEntry.text) || '';
81
+ const m = /^([ \t]*)/.exec(text);
82
+ if (!m || !m[1].length) continue;
83
+ const removeCount = Math.min(m[1].length, indentSize);
84
+ editorInfo.ace_replaceRange([i, 0], [i, removeCount], '');
85
+ didAnything = true;
86
+ }
87
+ if (!didAnything) return false;
88
+ evt.preventDefault();
89
+ return true;
90
+ };
91
+
92
+ exports.handleKey = (hookName, ctx) => {
93
+ if (!inCodeMode()) return false;
94
+ const evt = ctx && ctx.evt;
95
+ if (!evt || evt.type !== 'keydown') return false;
96
+ const rep = ctx.rep;
97
+ if (!rep || !rep.selStart || !rep.lines) return false;
98
+ const editorInfo = ctx.editorInfo;
99
+ if (!editorInfo || typeof editorInfo.ace_replaceRange !== 'function') return false;
100
+ if (evt.keyCode === 13 && !evt.ctrlKey && !evt.altKey && !evt.metaKey && !evt.shiftKey) {
101
+ return handleEnter(rep, editorInfo, evt);
102
+ }
103
+ if (evt.keyCode === 9 && !evt.ctrlKey && !evt.altKey && !evt.metaKey) {
104
+ return evt.shiftKey ? handleShiftTab(rep, editorInfo, evt) : handleTab(rep, editorInfo, evt);
105
+ }
106
+ return false;
107
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ MAX_LINES: 5000,
5
+ AUTO_REDETECT_MS: 2000,
6
+ LRU_CAPACITY: 2000,
7
+ };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ exports.showPausedBadge = (visible) => {
4
+ const sel = document.getElementById('ep_hljs_select');
5
+ if (!sel) return;
6
+ let badge = document.getElementById('ep_hljs_paused_badge');
7
+ if (visible && !badge) {
8
+ badge = document.createElement('span');
9
+ badge.id = 'ep_hljs_paused_badge';
10
+ badge.setAttribute('data-l10n-id', 'ep_hljs.paused');
11
+ badge.textContent = 'Highlighting paused';
12
+ sel.insertAdjacentElement('afterend', badge);
13
+ } else if (!visible && badge) {
14
+ badge.remove();
15
+ }
16
+ };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const lineRanges = new WeakMap();
4
+ const classHighlights = new Map();
5
+
6
+ const supports = (win) => {
7
+ if (!win || !win.Highlight) return false;
8
+ if (!win.CSS || !win.CSS.highlights) return false;
9
+ return typeof win.CSS.highlights.set === 'function';
10
+ };
11
+
12
+ const getOrCreateHighlight = (win, cls) => {
13
+ let h = classHighlights.get(cls);
14
+ if (h) return h;
15
+ h = new win.Highlight();
16
+ win.CSS.highlights.set(cls, h);
17
+ classHighlights.set(cls, h);
18
+ return h;
19
+ };
20
+
21
+ const buildSegments = (lineEl, win) => {
22
+ const doc = lineEl.ownerDocument;
23
+ const walker = doc.createTreeWalker(lineEl, win.NodeFilter.SHOW_TEXT);
24
+ const segs = [];
25
+ let pos = 0;
26
+ let n;
27
+ while ((n = walker.nextNode())) {
28
+ const len = n.nodeValue.length;
29
+ segs.push({node: n, start: pos, len});
30
+ pos += len;
31
+ }
32
+ return segs;
33
+ };
34
+
35
+ const buildRange = (doc, segs, start, end) => {
36
+ let startNode = null;
37
+ let startOff = 0;
38
+ let endNode = null;
39
+ let endOff = 0;
40
+ for (const seg of segs) {
41
+ const segEnd = seg.start + seg.len;
42
+ if (!startNode && start >= seg.start && start <= segEnd) {
43
+ startNode = seg.node;
44
+ startOff = start - seg.start;
45
+ }
46
+ // We break the moment endNode is set, so by the time we reach this
47
+ // condition again endNode is guaranteed null — no `!endNode` needed.
48
+ if (end > seg.start && end <= segEnd) {
49
+ endNode = seg.node;
50
+ endOff = end - seg.start;
51
+ break;
52
+ }
53
+ }
54
+ if (!startNode || !endNode) return null;
55
+ const range = doc.createRange();
56
+ try {
57
+ range.setStart(startNode, startOff);
58
+ range.setEnd(endNode, endOff);
59
+ } catch (_e) {
60
+ return null;
61
+ }
62
+ return range;
63
+ };
64
+
65
+ const removeLineRanges = (lineEl) => {
66
+ const map = lineRanges.get(lineEl);
67
+ if (!map) return;
68
+ for (const [cls, arr] of map) {
69
+ const h = classHighlights.get(cls);
70
+ if (!h) continue;
71
+ for (const r of arr) {
72
+ try { h.delete(r); } catch (_e) { /* stale range */ }
73
+ }
74
+ }
75
+ lineRanges.delete(lineEl);
76
+ };
77
+
78
+ const setLineRanges = (lineEl, tokenRanges) => {
79
+ if (!lineEl) return;
80
+ const win = lineEl.ownerDocument && lineEl.ownerDocument.defaultView;
81
+ if (!supports(win)) {
82
+ removeLineRanges(lineEl);
83
+ return;
84
+ }
85
+ removeLineRanges(lineEl);
86
+ if (!tokenRanges || !tokenRanges.length) return;
87
+ const segs = buildSegments(lineEl, win);
88
+ if (!segs.length) return;
89
+ const newMap = new Map();
90
+ for (const tr of tokenRanges) {
91
+ if (!tr || tr.start >= tr.end || !tr.cls) continue;
92
+ const range = buildRange(lineEl.ownerDocument, segs, tr.start, tr.end);
93
+ if (!range) continue;
94
+ const h = getOrCreateHighlight(win, tr.cls);
95
+ h.add(range);
96
+ let arr = newMap.get(tr.cls);
97
+ if (!arr) { arr = []; newMap.set(tr.cls, arr); }
98
+ arr.push(range);
99
+ }
100
+ if (newMap.size > 0) lineRanges.set(lineEl, newMap);
101
+ };
102
+
103
+ const clearAll = () => {
104
+ for (const h of classHighlights.values()) {
105
+ try { h.clear(); } catch (_e) { /* ignore */ }
106
+ }
107
+ };
108
+
109
+ module.exports = {setLineRanges, removeLineRanges, clearAll, buildRange, buildSegments};
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ // Parses hljs's HTML output into character ranges. hljs emits nested
4
+ // <span class="hljs-…">…</span> markup; we walk it linearly, accumulating
5
+ // the plain-text length and producing {start, end, cls} ranges for every
6
+ // closing </span>.
7
+ const SPAN_RE = /<span class="([^"]+)">|<\/span>/g;
8
+
9
+ // Single-pass entity decoder. A naive sequence of .replace(&amp;, &) followed
10
+ // by .replace(&lt;, <) double-decodes input like "&amp;lt;" into "<", which
11
+ // is wrong (it should stay as "&lt;"). One regex with a dispatch table
12
+ // processes each entity exactly once.
13
+ const ENTITIES = {
14
+ '&amp;': '&', '&lt;': '<', '&gt;': '>',
15
+ '&quot;': '"', '&#39;': "'", '&nbsp;': ' ',
16
+ };
17
+ const ENTITY_RE = /&(?:amp|lt|gt|quot|#39|nbsp);/g;
18
+ const decodeEntities = (s) => s.replace(ENTITY_RE, (m) => ENTITIES[m]);
19
+
20
+ // hljs sometimes emits multi-class spans (e.g. `<span class="hljs-meta hljs-string">`).
21
+ // CSS Highlights names are <custom-ident>, which can't contain spaces, so we
22
+ // emit one range per class and let the cascade overlay them.
23
+ const parseHljsHtml = (html) => {
24
+ const ranges = [];
25
+ const stack = [];
26
+ let pos = 0;
27
+ let plain = 0;
28
+ let m;
29
+ SPAN_RE.lastIndex = 0;
30
+ while ((m = SPAN_RE.exec(html)) != null) {
31
+ const before = decodeEntities(html.slice(pos, m.index));
32
+ plain += before.length;
33
+ if (m[1] != null) {
34
+ stack.push({classes: m[1].split(/\s+/).filter(Boolean), start: plain});
35
+ } else {
36
+ const top = stack.pop();
37
+ if (top) {
38
+ for (const cls of top.classes) {
39
+ ranges.push({start: top.start, end: plain, cls});
40
+ }
41
+ }
42
+ }
43
+ pos = m.index + m[0].length;
44
+ }
45
+ return ranges;
46
+ };
47
+
48
+ // Returns:
49
+ // Array<{start,end,cls}> — token ranges (possibly empty if no tokens)
50
+ // null — hljs not yet loaded; caller should skip + retry
51
+ const tokenize = (text, language) => {
52
+ if (!text) return [];
53
+ if (!language || language === 'auto' || language === 'off') return [];
54
+ const hljs = (typeof window !== 'undefined') ? window.hljs : null;
55
+ if (!hljs) return null;
56
+ if (!hljs.getLanguage(language)) return [];
57
+ let result;
58
+ try {
59
+ result = hljs.highlight(text, {language, ignoreIllegals: true});
60
+ } catch (_e) { return []; }
61
+ return parseHljsHtml(result.value);
62
+ };
63
+
64
+ const detect = (text) => {
65
+ const hljs = (typeof window !== 'undefined') ? window.hljs : null;
66
+ if (!hljs) return null;
67
+ let result;
68
+ try {
69
+ result = hljs.highlightAuto(text);
70
+ } catch (_e) { return null; }
71
+ if (!result || !result.language) return null;
72
+ // hljs scores by token-match count. A single short line of real code
73
+ // (e.g. `const foo = "bar"; // note`) typically scores 2-4. The previous
74
+ // threshold of 5 silently rejected most short pads. 2 is sensitive
75
+ // enough for real code without triggering on a single English word.
76
+ if ((result.relevance || 0) < 2) return null;
77
+ return result.language;
78
+ };
79
+
80
+ module.exports = {tokenize, detect, parseHljsHtml};