Skip to content

Commit bb311fe

Browse files
authored
Merge pull request #3415 from ably/dx-1128/docs-syntax-highlighter
DX-1128: localise syntax-highlighter util + registry
2 parents 50638fe + be09c27 commit bb311fe

6 files changed

Lines changed: 293 additions & 4 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@
8484
"gatsby-transformer-remark": "6.16.0",
8585
"gatsby-transformer-sharp": "5.16.0",
8686
"gatsby-transformer-yaml": "5.16.0",
87+
"highlight.js": "^11.11.1",
88+
"highlightjs-curl": "^1.3.0",
8789
"htmr": "^1.0.2",
8890
"js-yaml": "^4.2.0",
8991
"lodash": "^4.18.1",

src/components/Markdown/CodeBlock.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const mockHighlightSnippet = jest.fn();
2424
const mockParseLineHighlights = jest.fn();
2525
const mockSplitHtmlLines = jest.fn();
2626

27-
jest.mock('@ably/ui/core/utils/syntax-highlighter', () => ({
27+
jest.mock('src/utilities/syntax-highlighter', () => ({
2828
highlightSnippet: (...args: any[]) => mockHighlightSnippet(...args),
2929
LINE_HIGHLIGHT_CLASSES: {
3030
addition: 'code-line-addition',
@@ -36,7 +36,7 @@ jest.mock('@ably/ui/core/utils/syntax-highlighter', () => ({
3636
registerDefaultLanguages: jest.fn(),
3737
}));
3838

39-
jest.mock('@ably/ui/core/utils/syntax-highlighter-registry', () => ({
39+
jest.mock('src/utilities/syntax-highlighter-registry', () => ({
4040
__esModule: true,
4141
default: [],
4242
}));

src/components/Markdown/CodeBlock.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
registerDefaultLanguages,
77
parseLineHighlights,
88
splitHtmlLines,
9-
} from '@ably/ui/core/utils/syntax-highlighter';
10-
import languagesRegistry from '@ably/ui/core/utils/syntax-highlighter-registry';
9+
} from 'src/utilities/syntax-highlighter';
10+
import languagesRegistry from 'src/utilities/syntax-highlighter-registry';
1111

1212
registerDefaultLanguages(languagesRegistry);
1313

src/globals.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
11
declare module '*.png';
2+
3+
// highlightjs-curl ships no types; it's a highlight.js LanguageFn.
4+
declare module 'highlightjs-curl/src/languages/curl' {
5+
import type { LanguageFn } from 'highlight.js';
6+
const curl: LanguageFn;
7+
export default curl;
8+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// This file can be used in the browser, but because of the weight of all the language
2+
// definitions, preferably it should be used on the server.
3+
4+
import type { LanguageFn } from 'highlight.js';
5+
import bash from 'highlight.js/lib/languages/bash';
6+
import cpp from 'highlight.js/lib/languages/cpp';
7+
import csharp from 'highlight.js/lib/languages/csharp';
8+
import css from 'highlight.js/lib/languages/css';
9+
import dart from 'highlight.js/lib/languages/dart';
10+
import dos from 'highlight.js/lib/languages/dos';
11+
import diff from 'highlight.js/lib/languages/diff';
12+
import erlang from 'highlight.js/lib/languages/erlang';
13+
import elixir from 'highlight.js/lib/languages/elixir';
14+
import plaintext from 'highlight.js/lib/languages/plaintext';
15+
import go from 'highlight.js/lib/languages/go';
16+
import http from 'highlight.js/lib/languages/http';
17+
import java from 'highlight.js/lib/languages/java';
18+
import javascript from 'highlight.js/lib/languages/javascript';
19+
import typescript from 'highlight.js/lib/languages/typescript';
20+
import json from 'highlight.js/lib/languages/json';
21+
import objectivec from 'highlight.js/lib/languages/objectivec';
22+
import php from 'highlight.js/lib/languages/php';
23+
import python from 'highlight.js/lib/languages/python';
24+
import ruby from 'highlight.js/lib/languages/ruby';
25+
import swift from 'highlight.js/lib/languages/swift';
26+
import kotlin from 'highlight.js/lib/languages/kotlin';
27+
import sql from 'highlight.js/lib/languages/sql';
28+
import xml from 'highlight.js/lib/languages/xml';
29+
import yaml from 'highlight.js/lib/languages/yaml';
30+
import curl from 'highlightjs-curl/src/languages/curl';
31+
32+
const registry: { label: string; key: string; module: LanguageFn }[] = [
33+
{ label: 'Text', key: 'text', module: plaintext },
34+
{ label: 'JS', key: 'javascript', module: javascript },
35+
{ label: 'TS', key: 'typescript', module: typescript },
36+
{ label: 'Java', key: 'java', module: java },
37+
{ label: 'Ruby', key: 'ruby', module: ruby },
38+
{ label: 'Python', key: 'python', module: python },
39+
{ label: 'PHP', key: 'php', module: php },
40+
{ label: 'Shell', key: 'bash', module: bash },
41+
{ label: 'C#', key: 'cs', module: csharp },
42+
{ label: 'CSS', key: 'css', module: css },
43+
{ label: 'Go', key: 'go', module: go },
44+
{ label: 'HTML', key: 'xml', module: xml },
45+
{ label: 'HTTP', key: 'http', module: http },
46+
{ label: 'C++', key: 'cpp', module: cpp },
47+
{ label: 'Dart', key: 'dart', module: dart },
48+
{ label: 'Swift', key: 'swift', module: swift },
49+
{ label: 'Kotlin', key: 'kotlin', module: kotlin },
50+
{ label: 'Objective C', key: 'objectivec', module: objectivec },
51+
{ label: 'Node.js', key: 'javascript', module: javascript },
52+
{ label: 'JSON', key: 'json', module: json },
53+
{ label: 'DOS', key: 'dos', module: dos },
54+
{ label: 'YAML', key: 'yaml', module: yaml },
55+
{ label: 'Erlang', key: 'erlang', module: erlang },
56+
{ label: 'Elixir', key: 'elixir', module: elixir },
57+
{ label: 'Diff', key: 'diff', module: diff },
58+
{ label: 'SQL', key: 'sql', module: sql },
59+
{ label: 'cURL', key: 'curl', module: curl },
60+
{ label: 'HTML', key: 'html', module: xml },
61+
{ label: 'XML', key: 'xml', module: xml },
62+
];
63+
64+
export default registry;
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import hljs from 'highlight.js/lib/core';
2+
import type { LanguageFn } from 'highlight.js';
3+
4+
type LineHighlightType = 'addition' | 'removal' | 'highlight';
5+
6+
// Map certain frameworks, protocols etc to available language packs
7+
const languageToHighlightKey = (lang?: string): string => {
8+
let id: string | undefined;
9+
10+
if (!lang) {
11+
lang = 'text';
12+
}
13+
14+
switch (lang.toLowerCase()) {
15+
case 'android':
16+
id = 'java';
17+
break;
18+
19+
case '.net':
20+
case 'net':
21+
case 'dotnet':
22+
case 'csharp':
23+
case 'c#':
24+
id = 'cs';
25+
break;
26+
27+
case 'objc':
28+
case 'objective c':
29+
id = 'objectivec';
30+
break;
31+
32+
case 'laravel':
33+
id = 'php';
34+
break;
35+
36+
case 'flutter':
37+
id = 'dart';
38+
break;
39+
40+
case 'node.js':
41+
case 'js':
42+
id = 'javascript';
43+
break;
44+
45+
case 'ts':
46+
id = 'typescript';
47+
break;
48+
49+
case 'kotlin':
50+
case 'kt':
51+
id = 'kotlin';
52+
break;
53+
54+
case 'shell':
55+
case 'fh':
56+
case 'sh':
57+
id = 'bash';
58+
break;
59+
60+
case 'https':
61+
case 'http':
62+
case 'txt':
63+
case 'plaintext':
64+
id = 'text';
65+
break;
66+
67+
case 'cmd':
68+
case 'bat':
69+
id = 'dos';
70+
break;
71+
72+
case 'yml':
73+
id = 'yaml';
74+
break;
75+
76+
case 'erl':
77+
id = 'erlang';
78+
break;
79+
80+
case 'patch':
81+
id = 'diff';
82+
break;
83+
84+
case 'svg':
85+
id = 'xml';
86+
break;
87+
88+
default:
89+
break;
90+
}
91+
92+
return id || lang;
93+
};
94+
95+
const registerDefaultLanguages = (register: { key: string; module: LanguageFn }[]): void => {
96+
register.forEach(({ key, module }) => hljs.registerLanguage(key, module));
97+
};
98+
99+
const highlightSnippet = (languageKeyword: string | undefined, snippet: string): string | undefined => {
100+
const language = languageToHighlightKey(languageKeyword);
101+
if (typeof snippet !== 'string' || !snippet || !language) {
102+
return;
103+
}
104+
105+
return hljs.highlight(snippet, { language }).value;
106+
};
107+
108+
/**
109+
* Parse line highlight specifications from a meta string.
110+
*
111+
* Syntax: `highlight="+1-3,-5,7"`
112+
* - `+` prefix: addition (green)
113+
* - `-` prefix: removal (red)
114+
* - no prefix: neutral highlight (blue)
115+
* - `N-M`: inclusive line range
116+
* - comma-separated for multiple specs
117+
*/
118+
const parseLineHighlights = (
119+
languageString: string,
120+
meta?: string,
121+
): { lang: string; highlights: Record<number, LineHighlightType> } => {
122+
if (!meta) {
123+
return { lang: languageString, highlights: {} };
124+
}
125+
126+
const match = meta.match(/highlight=["']?([^"']+)["']?/);
127+
if (!match) {
128+
return { lang: languageString, highlights: {} };
129+
}
130+
131+
const spec = match[1];
132+
const highlights: Record<number, LineHighlightType> = {};
133+
134+
const tokens = spec.split(',');
135+
for (const token of tokens) {
136+
const trimmed = token.trim();
137+
if (!trimmed) {
138+
continue;
139+
}
140+
141+
let type: LineHighlightType = 'highlight';
142+
let rangePart = trimmed;
143+
144+
if (trimmed.startsWith('+')) {
145+
type = 'addition';
146+
rangePart = trimmed.slice(1);
147+
} else if (trimmed.startsWith('-')) {
148+
type = 'removal';
149+
rangePart = trimmed.slice(1);
150+
}
151+
152+
const rangeMatch = rangePart.match(/^(\d+)(?:-(\d+))?$/);
153+
if (!rangeMatch) {
154+
continue;
155+
}
156+
157+
const start = parseInt(rangeMatch[1], 10);
158+
const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : start;
159+
160+
for (let i = start; i <= end; i++) {
161+
highlights[i] = type;
162+
}
163+
}
164+
165+
return { lang: languageString, highlights };
166+
};
167+
168+
/**
169+
* Split highlighted HTML by newlines, repairing any spans that cross
170+
* line boundaries so each line fragment is valid HTML.
171+
*/
172+
const splitHtmlLines = (html: string): string[] => {
173+
const rawLines = html.split('\n');
174+
const result: string[] = [];
175+
const openTags: string[] = [];
176+
177+
for (const rawLine of rawLines) {
178+
let line = openTags.join('') + rawLine;
179+
180+
// Process open/close tags in document order
181+
const tagPattern = /<(\/?)span([^>]*)>/g;
182+
let m: RegExpExecArray | null;
183+
while ((m = tagPattern.exec(rawLine)) !== null) {
184+
if (m[1] === '/') {
185+
openTags.pop();
186+
} else {
187+
openTags.push(m[0]);
188+
}
189+
}
190+
191+
// Close any tags still open so this line is valid HTML
192+
for (let i = 0; i < openTags.length; i++) {
193+
line += '</span>';
194+
}
195+
196+
result.push(line);
197+
}
198+
199+
return result;
200+
};
201+
202+
const LINE_HIGHLIGHT_CLASSES: Record<LineHighlightType, string> = {
203+
addition: 'code-line-addition',
204+
removal: 'code-line-removal',
205+
highlight: 'code-line-highlight',
206+
};
207+
208+
export {
209+
highlightSnippet,
210+
languageToHighlightKey,
211+
LINE_HIGHLIGHT_CLASSES,
212+
parseLineHighlights,
213+
registerDefaultLanguages,
214+
splitHtmlLines,
215+
};
216+
export type { LineHighlightType };

0 commit comments

Comments
 (0)