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 #2015

Closed
Closed
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
54 changes: 53 additions & 1 deletion app/javascript/flavours/glitch/components/status_content.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import classnames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';

import highlightjs from 'highlight.js';

import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
Expand Down Expand Up @@ -311,6 +313,56 @@ class StatusContent extends PureComponent {
this.contentsNode = c;
};

/**
* 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 content with highlighted code inside code tags, or content if not found
*/
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;
}

render () {
const {
status,
Expand All @@ -329,7 +381,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: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
const content = { __html: status.getIn(['translation', 'contentHtml']) || this.highlightCode(status.get('contentHtml')) };
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';
6 changes: 3 additions & 3 deletions app/lib/advanced_text_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ def initialize(options, &block)
@format_link = block
end

def block_code(code, _language)
def block_code(code, language)
# Looks wrong, but sets title to language correctly. One downside is, it adds an empty attribute when no lang specified.
<<~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>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't suppose language is guaranteed to be escaped?

Copy link
Author

@Plastikmensch Plastikmensch Mar 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what kind of escaping would be suitable here.
At least in my testing I couldn't cause any unexpected behaviour, nevertheless I agree accepting user input as is is never a good idea.
Ideally, I would check here if language is a valid code language, but this would create overhead.
What I could do though is checking that language only contains allowed characters via regex: /^[a-zA-Z0-9.#+-]+$/

HTML
end

Expand Down Expand Up @@ -91,7 +92,6 @@ def rewrite
text_node.replace(replacement)
end
end

Sanitize.node!(@tree, Sanitize::Config::MASTODON_OUTGOING).to_html
end

Expand Down
Loading