diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx
index 67e5d53d830ce1..768f2bb140ab81 100644
--- a/app/javascript/flavours/glitch/components/status_content.jsx
+++ b/app/javascript/flavours/glitch/components/status_content.jsx
@@ -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';
@@ -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', {
diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss
index 1cb913c8b832ec..205b9d64108aa0 100644
--- a/app/javascript/flavours/glitch/styles/index.scss
+++ b/app/javascript/flavours/glitch/styles/index.scss
@@ -22,3 +22,4 @@
@import 'rtl';
@import 'dashboard';
@import 'rich_text';
+@import 'node_modules/highlight.js/scss/github-dark';
diff --git a/app/javascript/flavours/glitch/styles/mastodon-light.scss b/app/javascript/flavours/glitch/styles/mastodon-light.scss
index 8fc132651bdf67..40550254e099ec 100644
--- a/app/javascript/flavours/glitch/styles/mastodon-light.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon-light.scss
@@ -1,3 +1,4 @@
@import 'mastodon-light/variables';
@import 'index';
@import 'mastodon-light/diff';
+@import 'node_modules/highlight.js/scss/github';
diff --git a/app/javascript/flavours/glitch/utils/html.js b/app/javascript/flavours/glitch/utils/html.js
index 247e98c88a7f31..b20434be99178e 100644
--- a/app/javascript/flavours/glitch/utils/html.js
+++ b/app/javascript/flavours/glitch/utils/html.js
@@ -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(/
/g, '\n').replace(/<\/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
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;
+};
diff --git a/app/lib/advanced_text_formatter.rb b/app/lib/advanced_text_formatter.rb
index cdf1e2d9cd9394..1313d7ab4d511d 100644
--- a/app/lib/advanced_text_formatter.rb
+++ b/app/lib/advanced_text_formatter.rb
@@ -7,9 +7,9 @@ def initialize(options, &block)
@format_link = block
end
- def block_code(code, _language)
+ def block_code(code, language)
<<~HTML
-
#{ERB::Util.h(code).gsub("\n", '
')}
+ #{ERB::Util.h(code).gsub("\n", '
')}
HTML
end
diff --git a/config/code-languages.yml b/config/code-languages.yml
new file mode 100644
index 00000000000000..053de6a6d6465f
--- /dev/null
+++ b/config/code-languages.yml
@@ -0,0 +1,373 @@
+# Note: Valid languages must not include spaces.
+# When installing third-party languages, these have to be added here.
+languages:
+ - 1c
+ - abnf
+ - accesslog
+ - actionscript
+ - as
+ - ada
+ - angelscript
+ - asc
+ - apache
+ - apacheconf
+ - applescript
+ - osascript
+ - arcade
+ - arduino
+ - ino
+ - armasm
+ - arm
+ - xml
+ - html
+ - xhtml
+ - rss
+ - atom
+ - xjb
+ - xsd
+ - xsl
+ - plist
+ - wsf
+ - svg
+ - asciidoc
+ - adoc
+ - aspectj
+ - autohotkey
+ - ahk
+ - autoit
+ - avrasm
+ - awk
+ - axapta
+ - x++
+ - bash
+ - sh
+ - basic
+ - bnf
+ - brainfuck
+ - bf
+ - c
+ - h
+ - cal
+ - capnproto
+ - capnp
+ - ceylon
+ - clean
+ - icl
+ - dcl
+ - clojure
+ - clj
+ - edn
+ - clojure-repl
+ - cmake
+ - cmake.in
+ - coffeescript
+ - coffee
+ - cson
+ - iced
+ - coq
+ - cos
+ - cls
+ - cpp
+ - cc
+ - c++
+ - h++
+ - hpp
+ - hh
+ - hxx
+ - cxx
+ - crmsh
+ - crm
+ - pcmk
+ - crystal
+ - cr
+ - csharp
+ - cs
+ - c#
+ - csp
+ - css
+ - d
+ - markdown
+ - md
+ - mkdown
+ - mkd
+ - dart
+ - delphi
+ - dpr
+ - dfm
+ - pas
+ - pascal
+ - diff
+ - patch
+ - django
+ - jinja
+ - dns
+ - bind
+ - zone
+ - dockerfile
+ - docker
+ - dos
+ - bat
+ - cmd
+ - dsconfig
+ - dts
+ - dust
+ - dst
+ - ebnf
+ - elixir
+ - ex
+ - exs
+ - elm
+ - ruby
+ - rb
+ - gemspec
+ - podspec
+ - thor
+ - irb
+ - erb
+ - erlang-repl
+ - erlang
+ - erl
+ - excel
+ - xlsx
+ - xls
+ - fix
+ - flix
+ - fortran
+ - f90
+ - f95
+ - fsharp
+ - fs
+ - f#
+ - gams
+ - gms
+ - gauss
+ - gss
+ - gcode
+ - nc
+ - gherkin
+ - feature
+ - glsl
+ - gml
+ - go
+ - golang
+ - golo
+ - gradle
+ - graphql
+ - gql
+ - groovy
+ - haml
+ - handlebars
+ - hbs
+ - html.hbs
+ - html.handlebars
+ - htmlbars
+ - haskell
+ - hs
+ - haxe
+ - hx
+ - hsp
+ - http
+ - https
+ - hy
+ - hylang
+ - inform7
+ - i7
+ - ini
+ - toml
+ - irpf90
+ - isbl
+ - java
+ - jsp
+ - javascript
+ - js
+ - jsx
+ - mjs
+ - cjs
+ - jboss-cli
+ - wildfly-cli
+ - json
+ - julia
+ - julia-repl
+ - jldoctest
+ - kotlin
+ - kt
+ - kts
+ - lasso
+ - ls
+ - lassoscript
+ - latex
+ - tex
+ - ldif
+ - leaf
+ - less
+ - lisp
+ - livecodeserver
+ - livescript
+ - ls
+ - llvm
+ - lsl
+ - lua
+ - makefile
+ - mk
+ - mak
+ - make
+ - mathematica
+ - mma
+ - wl
+ - matlab
+ - maxima
+ - mel
+ - mercury
+ - m
+ - moo
+ - mipsasm
+ - mips
+ - mizar
+ - perl
+ - pl
+ - pm
+ - mojolicious
+ - monkey
+ - moonscript
+ - moon
+ - n1ql
+ - nestedtext
+ - nt
+ - nginx
+ - nginxconf
+ - nim
+ - nix
+ - nixos
+ - node-repl
+ - nsis
+ - objectivec
+ - mm
+ - objc
+ - obj-c
+ - obj-c++
+ - objective-c++
+ - ocaml
+ - ml
+ - openscad
+ - scad
+ - oxygene
+ - parser3
+ - pf
+ - pf.conf
+ - pgsql
+ - postgres
+ - postgresql
+ - php
+ - php-template
+ - plaintext
+ - text
+ - txt
+ - pony
+ - powershell
+ - pwsh
+ - ps
+ - ps1
+ - processing
+ - pde
+ - profile
+ - prolog
+ - properties
+ - protobuf
+ - proto
+ - puppet
+ - pp
+ - purebasic
+ - pb
+ - pbi
+ - python
+ - py
+ - gyp
+ - ipython
+ - python-repl
+ - pycon
+ - q
+ - k
+ - kdb
+ - qml
+ - qt
+ - r
+ - reasonml
+ - re
+ - rib
+ - roboconf
+ - graph
+ - instances
+ - routeros
+ - mikrotik
+ - rsl
+ - ruleslanguage
+ - rust
+ - rs
+ - sas
+ - scala
+ - scheme
+ - scm
+ - scilab
+ - sci
+ - scss
+ - shell
+ - console
+ - shellsession
+ - smali
+ - smalltalk
+ - st
+ - sml
+ - ml
+ - sqf
+ - sql
+ - stan
+ - stanfuncs
+ - stata
+ - do
+ - ado
+ - step21
+ - p21
+ - step
+ - stp
+ - stylus
+ - styl
+ - subunit
+ - swift
+ - taggerscript
+ - yaml
+ - yml
+ - tap
+ - tcl
+ - tk
+ - thrift
+ - tp
+ - twig
+ - craftcms
+ - typescript
+ - ts
+ - tsx
+ - mts
+ - cts
+ - vala
+ - vbnet
+ - vb
+ - vbscript
+ - vbs
+ - vbscript-html
+ - verilog
+ - v
+ - sv
+ - svh
+ - vhdl
+ - vim
+ - wasm
+ - wren
+ - x86asm
+ - xl
+ - tao
+ - xquery
+ - xpath
+ - xq
+ - xqm
+ - zephir
+ - zep
diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb
index 53508d3e45153e..99705299d2cbac 100644
--- a/lib/sanitize_ext/sanitize_config.rb
+++ b/lib/sanitize_ext/sanitize_config.rb
@@ -21,6 +21,16 @@ module Config
gemini
).freeze
+ # Valid code languages for highlight.js
+ VALID_LANGUAGES = YAML.load_file(File.expand_path('../../config/code-languages.yml', __dir__))['languages'].freeze
+
+ DATA_LANG_TRANSFORMER = lambda do |env|
+ return unless env[:node_name] == 'code' && env[:node]['data-codelang']
+
+ node = env[:node]
+ node.remove_attribute('data-codelang') unless VALID_LANGUAGES.include?(node['data-codelang'].downcase)
+ end
+
CLASS_WHITELIST_TRANSFORMER = lambda do |env|
node = env[:node]
class_list = node['class']&.split(/[\t\n\f\r ]/)
@@ -84,6 +94,7 @@ module Config
'blockquote' => %w(cite),
'ol' => %w(start reversed),
'li' => %w(value),
+ 'code' => %w(data-codelang),
},
add_attributes: {
@@ -103,6 +114,7 @@ module Config
IMG_TAG_TRANSFORMER,
TRANSLATE_TRANSFORMER,
UNSUPPORTED_HREF_TRANSFORMER,
+ DATA_LANG_TRANSFORMER,
]
)
@@ -169,6 +181,7 @@ module Config
UNSUPPORTED_HREF_TRANSFORMER,
LINK_REL_TRANSFORMER,
LINK_TARGET_TRANSFORMER,
+ DATA_LANG_TRANSFORMER,
]
)
end
diff --git a/package.json b/package.json
index 40578504e822bb..ee5690ddd98ba6 100644
--- a/package.json
+++ b/package.json
@@ -50,9 +50,9 @@
"@renchap/compression-webpack-plugin": "^6.1.4",
"@svgr/webpack": "^5.5.0",
"abortcontroller-polyfill": "^1.7.5",
- "atrament": "0.2.4",
"arrow-key-navigation": "^1.2.0",
"async-mutex": "^0.4.0",
+ "atrament": "0.2.4",
"autoprefixer": "^10.4.14",
"axios": "^1.4.0",
"babel-loader": "^8.3.0",
@@ -80,6 +80,7 @@
"font-awesome": "^4.7.0",
"fuzzysort": "^2.0.4",
"glob": "^10.2.6",
+ "highlight.js": "^11.9.0",
"history": "^4.10.1",
"hoist-non-react-statics": "^3.3.2",
"http-link-header": "^1.1.1",
diff --git a/spec/lib/advanced_text_formatter_spec.rb b/spec/lib/advanced_text_formatter_spec.rb
index f92385219615fe..54602f77a2a45e 100644
--- a/spec/lib/advanced_text_formatter_spec.rb
+++ b/spec/lib/advanced_text_formatter_spec.rb
@@ -50,6 +50,14 @@
it 'does not format links' do
expect(subject).to include 'return 0; // https://joinmastodon.org/foo'
end
+
+ context 'with valid language' do
+ let(:text) { "test\n\n```c++\nint main(void) {\n return 0; // https://joinmastodon.org/foo\n}\n```\n" }
+
+ it 'formats code using and with data containing set language' do
+ expect(subject).to include 'int main'
+ end
+ end
end
context 'with a link in inline code using backticks' do
diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb
index cc9916bfd40e4a..1a65fb1aa0ccd0 100644
--- a/spec/lib/sanitize_config_spec.rb
+++ b/spec/lib/sanitize_config_spec.rb
@@ -55,6 +55,14 @@
it 'keeps title in abbr' do
expect(Sanitize.fragment('HTML', subject)).to eq 'HTML'
end
+
+ it 'keeps data-codelang attribute in code' do
+ expect(Sanitize.fragment('int main(void) { return 0; // https://joinmastodon.org/foo }
', subject)).to eq 'int main(void) { return 0; // https://joinmastodon.org/foo }
'
+ end
+
+ it 'removes data-codelang attribute in code when unsupported' do
+ expect(Sanitize.fragment('int main(void) { return 0; // https://joinmastodon.org/foo }
', subject)).to eq 'int main(void) { return 0; // https://joinmastodon.org/foo }
'
+ end
end
describe '::MASTODON_OUTGOING' do
diff --git a/yarn.lock b/yarn.lock
index 1f23f26ddee4b5..b1759355f5100d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6617,6 +6617,11 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
+highlight.js@^11.9.0:
+ version "11.9.0"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
+ integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
+
history@^4.10.1, history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"