Skip to content

Commit

Permalink
feat: unify code highlight [WPB-12089] (#18526)
Browse files Browse the repository at this point in the history
* feat: unity code highlight

* refactor: create highlighCode utils

* refactor(lexical-input): change color format for code theme
  • Loading branch information
olafsulich authored Dec 19, 2024
1 parent 7740526 commit 7fef0b1
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 63 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"dexie-batch": "0.4.3",
"dexie-encrypted": "2.0.0",
"emoji-picker-react": "4.12.0",
"highlight.js": "11.11.0",
"http-status-codes": "2.3.0",
"jimp": "0.22.12",
"js-cookie": "3.0.5",
Expand All @@ -43,6 +42,8 @@
"murmurhash": "2.0.1",
"oidc-client-ts": "3.1.0",
"platform": "1.3.6",
"prism-themes": "^1.9.0",
"prismjs": "^1.29.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-error-boundary": "4.1.2",
Expand Down Expand Up @@ -91,6 +92,7 @@
"@types/node": "22.9.0",
"@types/open-graph": "0.2.5",
"@types/platform": "1.3.6",
"@types/prismjs": "^1",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.1",
"@types/react-transition-group": "4.4.12",
Expand Down
38 changes: 36 additions & 2 deletions src/script/components/RichTextEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {FormatToolbar} from './components/FormatToolbar/FormatToolbar';
import {EmojiNode} from './nodes/EmojiNode';
import {MentionNode} from './nodes/MentionNode';
import {AutoFocusPlugin} from './plugins/AutoFocusPlugin';
import {CodeHighlightPlugin} from './plugins/CodeHighlightPlugin/CodeHighlightPlugin';
import {DraftStatePlugin} from './plugins/DraftStatePlugin';
import {EditedMessagePlugin} from './plugins/EditedMessagePlugin/EditedMessagePlugin';
import {EmojiPickerPlugin} from './plugins/EmojiPickerPlugin';
Expand Down Expand Up @@ -75,7 +76,7 @@ const theme = {
italic: 'editor-italic',
underline: 'editor-underline',
strikethrough: 'editor-strikethrough',
code: 'editor-code',
code: 'editor-inline-code',
},
list: {
ul: 'editor-list editor-list-unordered',
Expand All @@ -91,6 +92,39 @@ const theme = {
h2: 'editor-heading editor-heading--2',
h3: 'editor-heading editor-heading--3',
},
code: 'editor-code',
codeHighlight: {
atrule: 'editor-tokenAtrule',
attr: 'editor-tokenAttr',
boolean: 'editor-tokenBoolean',
builtin: 'editor-tokenBuiltin',
cdata: 'editor-tokenCdata',
char: 'editor-tokenChar',
class: 'editor-tokenClass',
'class-name': 'editor-tokenClassName',
comment: 'editor-tokenComment',
constant: 'editor-tokenConstant',
deleted: 'editor-tokenDeleted',
doctype: 'editor-tokenDoctype',
entity: 'editor-tokenEntity',
function: 'editor-tokenFunction',
important: 'editor-tokenImportant',
inserted: 'editor-tokenInserted',
keyword: 'editor-tokenKeyword',
namespace: 'editor-tokenNamespace',
number: 'editor-tokenNumber',
operator: 'editor-tokenOperator',
prolog: 'editor-tokenProlog',
property: 'editor-tokenProperty',
punctuation: 'editor-tokenPunctuation',
regex: 'editor-tokenRegex',
selector: 'editor-tokenSelector',
string: 'editor-tokenString',
symbol: 'editor-tokenSymbol',
tag: 'editor-tokenTag',
url: 'editor-tokenUrl',
variable: 'editor-tokenVariable',
},
};

export type RichTextContent = {
Expand Down Expand Up @@ -227,7 +261,7 @@ export const RichTextEditor = ({

<ReplaceCarriageReturnPlugin />
<MarkdownShortcutPlugin transformers={markdownTransformers} />

<CodeHighlightPlugin />
<RichTextPlugin
contentEditable={<ContentEditable className="conversation-input-bar-text" data-uie-name="input-message" />}
placeholder={<Placeholder text={placeholder} hasLocalEphemeralTimer={hasLocalEphemeralTimer} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {useEffect} from 'react';

import {registerCodeHighlighting} from '@lexical/code';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

export const CodeHighlightPlugin = (): null => {
const [editor] = useLexicalComposerContext();

useEffect(() => {
return registerCodeHighlighting(editor);
}, [editor]);

return null;
};
46 changes: 46 additions & 0 deletions src/script/util/highlightCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import Prism from 'prismjs';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-csv';
import 'prismjs/components/prism-haskell';
import 'prismjs/components/prism-java';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-kotlin';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-regex';
import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-yaml';

interface HighlightCodeParams {
code: string;
grammar: Prism.Grammar;
lang: string;
}

export const highlightCode = ({code, grammar, lang}: HighlightCodeParams) => {
// eslint-disable-next-line import/no-named-as-default-member
return Prism.highlight(code, grammar, lang);
};

// eslint-disable-next-line import/no-named-as-default-member
export const languages = Prism.languages;
52 changes: 6 additions & 46 deletions src/script/util/messageRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,59 +497,19 @@ describe('Markdown for code snippets', () => {
});

it(`doesn't render links within code blocks`, () => {
const expected =
'<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>\n <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>com.ibm.icu<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>\n <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>icu4j<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>\n <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>53.1<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>\n<span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>\n</code></pre>';
const expected = `<pre><code class="lang-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dependency</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>groupId</span><span class="token punctuation">></span></span>com.ibm.icu<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>groupId</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>artifactId</span><span class="token punctuation">></span></span>icu4j<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>artifactId</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>version</span><span class="token punctuation">></span></span>53.1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>version</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dependency</span><span class="token punctuation">></span></span>
</code></pre>`;

expect(
renderMessage(
'```xml\n<dependency>\n <groupId>com.ibm.icu</groupId>\n <artifactId>icu4j</artifactId>\n <version>53.1</version>\n</dependency>\n```',
),
).toEqual(expected);
});

it('renders escaped Ruby code blocks', () => {
const expected =
'<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">&#x27;redcarpet&#x27;</span>\nmarkdown = <span class="hljs-title class_">Redcarpet</span>.new(<span class="hljs-string">&quot;Hello World!&quot;</span>)\nputs markdown.to_html\n</code></pre>';

expect(
renderMessage(
'```ruby\nrequire \'redcarpet\'\nmarkdown = Redcarpet.new("Hello World!")\nputs markdown.to_html\n```',
),
).toEqual(expected);
});

it('renders escaped JavaScript code blocks', () => {
const expected =
'<pre><code class="lang-js">$(<span class="hljs-variable language_">document</span>).<span class="hljs-title function_">ready</span>(<span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) {\n $(<span class="hljs-string">&#x27;pre code&#x27;</span>).<span class="hljs-title function_">each</span>(<span class="hljs-keyword">function</span>(<span class="hljs-params">i, block</span>) {\n hljs.<span class="hljs-title function_">highlightBlock</span>(block);\n });\n});\n</code></pre>';
expect(
renderMessage(
"```js\n$(document).ready(function() {\n $('pre code').each(function(i, block) {\n hljs.highlightBlock(block);\n });\n});\n```",
),
).toEqual(expected);
});

it('renders escaped TypeScript code blocks', () => {
const expected =
'<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> greetings = (<span class="hljs-attr">name</span>: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">string</span> =&gt;</span> {\n <span class="hljs-keyword">return</span> <span class="hljs-string">`Hello, <span class="hljs-subst">${name}</span>!`</span>;\n};\n<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-title function_">greetings</span>(<span class="hljs-string">&#x27;world&#x27;</span>));\n</code></pre>';
expect(
renderMessage(
"```typescript\nconst greetings = (name: string): string => {\n return `Hello, ${name}!`;\n};\nconsole.log(greetings('world'));\n```",
),
).toEqual(expected);
});

it('renders escaped HTML code blocks', () => {
const expected =
'<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;javascript:wire.app.logout()&quot;</span>&gt;</span>This is a trick<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>\n</code></pre>';

expect(renderMessage('```html\n<a href="javascript:wire.app.logout()">This is a trick</a>\n```')).toEqual(expected);
});

it('renders escaped HTML code spans', () => {
const expected = '<code>&lt;a href=&quot;javascript:wire.app.logout()&quot;&gt;This is a trick&lt;/a&gt;</code>';

expect(renderMessage('`<a href="javascript:wire.app.logout()">This is a trick</a>`')).toEqual(expected);
});
});

describe('Markdown for headings', () => {
Expand Down
9 changes: 7 additions & 2 deletions src/script/util/messageRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
*/

import {QualifiedId} from '@wireapp/api-client/lib/user';
import hljs from 'highlight.js';
import MarkdownIt from 'markdown-it';
import {escape} from 'underscore';

import {highlightCode, languages} from './highlightCode';
import {replaceInRange} from './StringUtil';

import type {MentionEntity} from '../message/MentionEntity';
Expand Down Expand Up @@ -179,7 +179,12 @@ export const renderMessage = (message: string, selfId?: QualifiedId, mentionEnti
// highlighting will be wrong anyway because this is not valid code
return escape(code);
}
return hljs.highlightAuto(code, lang ? [lang] : undefined).value;

if (lang && languages[lang]) {
return highlightCode({code, grammar: languages[lang], lang});
}

return highlightCode({code, grammar: languages.javascript, lang: 'javascript'});
},
});

Expand Down
Loading

0 comments on commit 7fef0b1

Please sign in to comment.