Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add syntax highlighting to code tags #2457

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/javascript/flavours/glitch/components/status_content.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { connect } from 'react-redux';

import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { highlightCode } from 'flavours/glitch/utils/html';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';

import Permalink from './permalink';
Expand Down Expand Up @@ -345,7 +346,7 @@ class StatusContent extends PureComponent {
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);

const content = { __html: statusContent ?? getStatusContent(status) };
const content = { __html: highlightCode(statusContent ?? getStatusContent(status)) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {
Expand Down
1 change: 1 addition & 0 deletions app/javascript/flavours/glitch/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
@import 'rtl';
@import 'dashboard';
@import 'rich_text';
@import 'node_modules/highlight.js/scss/github-dark';
1 change: 1 addition & 0 deletions app/javascript/flavours/glitch/styles/mastodon-light.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import 'mastodon-light/variables';
@import 'index';
@import 'mastodon-light/diff';
@import 'node_modules/highlight.js/scss/github';
52 changes: 52 additions & 0 deletions app/javascript/flavours/glitch/utils/html.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
import highlightjs from 'highlight.js';

// NB: This function can still return unsafe HTML
export const unescapeHTML = (html) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, '');
return wrapper.textContent;
};

/**
* Highlights code in code tags.\
* Uses highlight.js to convert content inside code tags to span elements with class attributes
* @param {string} content - String containing html code tags
* @returns {string} content with highlighted code inside code tags, or content if not highlighted
*/
export const highlightCode = (content) => {
// highlightJS complains when unescaped html is given
highlightjs.configure({ ignoreUnescapedHTML: true });

// Create a new temporary element to work on
const wrapper = document.createElement('div');
wrapper.innerHTML = content;

// Get code elements and run highlightJS on each.
wrapper.querySelectorAll('code')
.forEach((code) => {
// Get language from data attribute containing code language of code element
let lang = highlightjs.getLanguage(code.dataset.codelang);

// Check if lang is a valid language
if (lang !== undefined) {
// Set codelang as class attribute, since highlightElement cannot be given a language
// highlightJS will read this attribute and use it to highlight in the proper language
code.setAttribute('class', code.dataset.codelang);

// Set title attribute to language name, i.e. "js" will become "Javascript"
code.setAttribute('title', lang.name);

// Replace <br> as highlightJS removes them, messing up formatting
let brTags = Array.from(code.getElementsByTagName('br'));
for (let br of brTags) {
br.replaceWith('\n');
}

// Highlight the code element
highlightjs.highlightElement(code);

// highlightJS adds own class attribute, remove it again to not mess up styling
code.removeAttribute('class');
} else {
// Remove data attribute as it's not a valid language.
delete code.dataset.codelang;
}
});

// return content with highlighted code
return wrapper.innerHTML;
};
4 changes: 2 additions & 2 deletions app/lib/advanced_text_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ def initialize(options, &block)
@format_link = block
end

def block_code(code, _language)
def block_code(code, language)
<<~HTML
<pre><code>#{ERB::Util.h(code).gsub("\n", '<br/>')}</code></pre>
<pre><code data-codelang="#{language}">#{ERB::Util.h(code).gsub("\n", '<br/>')}</code></pre>
HTML
end

Expand Down
Loading
Loading