Discourse syntax highlighting?

Surely it’s not just me… Does anyone else feel that the syntax highlighting for Julia on Discourse looks a bit dull compared to other languages?

For instance, here’s some simple Julia code:

struct Vec{T<:Real}
    x::T  # foo
    y::T  # bar
end

Base.:+(a::Vec, b::Vec) = Vec(a.x + b.x, a.y + b.y)

α = 1.23
v = Vec(1.0, α) + Vec(3.0, 4.0)

mode = :fast

if mode == :fast
    @info v
end
[Here's a screenshot of this on my machine.]

And now a similar snippet translated to JavaScript:

class Vec {
    constructor(x, y) {
        this.x = x;  // foo
        this.y = y;  // bar
    }

    add(other) {
        return new Vec(this.x + other.x, this.y + other.y);
    }
}

const α = 1.23;
const v = new Vec(1.0, α).add(new Vec(3.0, 4.0));

const mode = "fast";

if (mode === "fast") {
    console.log(v);
}
[Here's a screenshot of this on my machine.]

Maybe it’s a small thing, but given how much the Julia community relies on Discourse, it seems odd that Julia’s syntax highlighting feels noticeably less clear or vibrant than other languages on the Julia forum itself. Is there any way to improve it? How configurable is Discourse?

OhMyREPL highlighting in my terminal, for reference:

7 Likes

Thanks. Will post here to not ping everybody on that thread

@mbauman do you know if there are any updates to what things we have control over on Discourse? I feel like Discourse is popular enough for someone somewhere to figure out how to customize code highlighting CSS for a specific community

@mbauman check out this:

1 Like

@fredrikekre @mbauman

Also looks like there’s a way to override highlight.js here:

2 Likes

I am glad someone finally points out the issue here. Thanks @MilesCranmer for putting your efforts into this issue.

2 Likes

I prefer “dull” to “so flashy as to hinder readability”. Definitely prefer the way Julia code is highlighted to your JavaScript example, the latter is much less readable to me.

1 Like

It’s definitely too dull now, imo. I especially dislike that it only seems to highlight some predefined list of types, e.g.

foo(x::Real)
foo(x::Vec)

And zero highlighting here?

Base.:+(a::Vec, b::Vec) = Vec(a.x + b.x, a.y + b.y)
7 Likes

Agree, special-casing is ugly. However, given that types are normal values in Julia, I think the only choice is not to attempt to treat types differently than other kinds of values.

We could do a poll

Is the syntax highlighting of Julia:

  • Fine as is
  • Too dull
0 voters
1 Like

The right side of the :: operator generally needs to be a Type:

julia> 1::1
ERROR: TypeError: in typeassert, expected Type, got a value of type Int64

julia> foo(x::1) = 42
ERROR: ArgumentError: invalid type for argument x in method definition for foo at REPL[6]:1

So it’s reasonable to use a “type scope” to highlight the RHS of ::, which is what the Julia extenstion in VS Code does:

EDIT:

Though I see that it doesn’t work quite as well as one might like. Not sure if this is fixable:

Here you can see that typeof is assigned the support.type.julia TextMate scope, which is not quite right.

You’re probably right that, strictly speaking, any expression can be used on the right-hand side of ::, so we shouldn’t really color those expressions differently, but practically speaking it is pretty nice to give the RHS (which is usually a type) a different color.

I’m always game to try stuff out. We are a CDCK-hosted discourse here, so we are more constrained than a typical self-hosted install. However, we can edit themes and theme components. I had been thinking that we were limited to just css styling with the default hljs tagging, but to change anything meaningful, we need a new hljs javascript spec.

Best of all worlds (and simplest!) would be to upstream Fredrik’s work from Improved syntax highlighting for Julia on the web | Fredrik Ekre. It looks like that had been blocked on Safari (ok since 27 March 2023) and then v12 of hljs (still at v12.0.0-pre right now).

It does look like we can add custom language support via custom JS in a theme or theme component. I’m not clear if that means we can also override Julia’s default hljs highlighting, but I’d guess we could. I don’t know how though. To be concrete, I’d need to know what gets plopped here:

Can you turn off languages? My guess is that you would disable Julia, and then add a “custom language” which is just Julia but with the improved syntax highlighting.

Yes, but again, I don’t know the order of effects here:

Is there a /admin/customize/themes page? That should let you create a theme. Then you should be able to preview it while editing.

Also is there a /admin/customize/themes/components page?

I think it should basically look like this though:

  1. Delete julia and julia-repl from the list of highlighted languages in your screenshot.
  2. Insert the following code from this how-to guide Install a new language for Highlight.JS via a theme component - Developer Guides - Discourse Meta
import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
    const juliaLang = function(e){
        // Fredrik's code
    }
    api.registerHighlightJSLanguage("julia", juliaLang);
    api.registerHighlightJSLanguage("julia-repl", juliaLang);
    // Or if there's a custom REPL highlighter, use that?
});

Seems the same as what glimmer.js does: discourse-highlightjs-glimmer/javascripts/discourse/api-initializers/init-highlightjs-glimmer.js at main · discourse/discourse-highlightjs-glimmer · GitHub

import { apiInitializer } from "discourse/lib/api";
import { glimmer, glimmerJavascript } from "../../vendor/highlightjs-glimmer";

export default apiInitializer("0.8", (api) => {
  api.registerHighlightJSLanguage("glimmer", glimmer);
  api.registerHighlightJSLanguage("glimmer-javascript", glimmerJavascript);
});

where the highlightjs-glimmer file is here: discourse-highlightjs-glimmer/javascripts/vendor/highlightjs-glimmer/index.js at main · discourse/discourse-highlightjs-glimmer · GitHub

Formatted version to save your eyes:
function o(n) {
  function t() {
    return {
      name: 'Ember.JS, Glimmer',
          aliases:
              ['glimmer', 'hbs', 'html.hbs', 'html.handlebars', 'htmlbars'],
          case_insensitive: !0, keywords: E, disableAutodetect: !0, contains: [
            n.COMMENT(/\{\{!--/, /--\}\}/), n.COMMENT(/\{\{!/, /\}\}/),
            n.COMMENT(/<!--/, /-->/), d, ...l, g, u, S, ...H
          ]
    }
  }
  let E = {
    $pattern: /\b[\w][\w-]+\b/,
    keyword: ''.concat('outlet yield', ' ')
                 .concat('action on', ' ')
                 .concat('log debugger'),
    built_in: 'let each each-in if else unless',
    function:
        ''.concat('not-eq xor is-array is-object is-equal', ' ')
            .concat(
                'has-block concat fn component helper modifier get hash query-params',
                ' ')
            .concat('eq neq', ' ')
            .concat('gt gte le lte', ' ')
            .concat('and or not'),
    literal: 'true false undefined null'
  },
      f = e.either(
          e.concat(/[a-zA-Z_]/, e.optional(/[A-Z0-9:_.-]*:/), /[A-Z0-9_.-]*/),
          /[a-z]/),
      _ = /[A-Z][A-Za-z0-9]+/,
      I = e.either(
          _, /[a-zA-Z0-9]*\.[a-zA-Z0-9-]*/, e.concat(_, /::/, /-?/, _),
          /[a-z]/),
      P = /[a-z-][a-z\d-_]+\b/, C = /[@A-Za-z0-9._:-]+/,
      d = {className: 'symbol', begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},
      G = {
        className: 'punctuation',
        match: e.either(
            /\./, /\{\{\{?#?\/?/, /\}\}\}?/, /\(/, /\)/, /::/, /\|/, /~/)
      },
      L = {
        begin: /as\s+\|/,
        keywords: {keyword: 'as'},
        end: /\|/,
        contains: [{className: 'template-variable', begin: C}]
      },
      g = {className: 'operator', match: /=/}, u = {
        className: {1: 'punctuation', 2: 'params'},
        match: [/@/, /[\w\d-_]+/]
      },
      N = {
        className: {1: 'attribute', 2: 'operator'},
        match: [/[A-Za-z0-9-_]+/, /=/]
      },
      R = {
        className: {1: 'class', 2: 'punctuation', 3: 'property'},
        match: [/this/, /\./, /[^\s}]+/]
      },
      A = {className: 'title', match: I},
      U = {className: 'title', match: P, keywords: E},
      x = {className: 'number', match: /[\d]+((\.[\d]+))?/}, S = {
        className: 'comment',
        begin: /\{\{!--/,
        contains: [
          {className: 'comment', match: /.+/}, {begin: /--\}\}/, endsParent: !0}
        ]
      },
      m = {
        className: 'string',
        variants: [
          {begin: /"/, end: /"/, contains: [d]},
          {begin: /'/, end: /'/, contains: [d]}
        ]
      },
      p = [G, g, u, x, L, R, N, U, m], O = {
        keywords: E,
        begin: e.concat(/\(/, e.lookahead(e.concat(/\)/))),
        end: /\)/,
        contains: [...p, 'self', {begin: /\)/, endsParent: !0}]
      };
  p.push(O);
  let l = [{
    className: 'punctuation mustache',
    keywords: E,
    begin: e.concat(/\{\{\{?#?/),
    end: /\}\}\}?/,
    contains: [{begin: /\}\}\}?/, endsParent: !0}, ...p, O]
  }];
  m.variants.forEach(v => v.contains.push(...l));
  let H = [
    {
      className: 'tag',
      begin: e.concat(
          /<:?/, e.lookahead(e.concat('style', e.either(/\/>/, />/, /\s/)))),
      end: /\/?>/,
      contains: [g, u, S, L, R, ...l, N, m, A],
      starts: {
        className: 'tag',
        end: /<\/style>/,
        returnEnd: !0,
        excludeEnd: !1,
        subLanguage: ['css', 'glimmer']
      }
    },
    {
      className: 'tag',
      begin:
          e.concat(/<:?/, e.lookahead(e.concat(f, e.either(/\/>/, />/, /\s/)))),
      end: /\/?>/,
      contains: [g, u, S, L, R, ...l, N, m, A]
    },
    {
      className: 'tag',
      begin: e.concat(/<\/:?/, e.lookahead(e.concat(f, />/))),
      end: />/,
      contains: [A]
    }
  ];
  return t()
}
function y(n) {
  return T('(?=', n, ')')
}
function w(n) {
  return T('(', n, ')?')
}
function T(...n) {
  return n.map(a => M(a)).join('')
}
function j(...n) {
  return '(' + n.map(a => M(a)).join('|') + ')'
}
function M(n) {
  return n ? typeof n == 'string' ? n : n.source : null
}
var e = {lookahead: y, either: j, optional: w, concat: T};
var k = ['css`', '.?css`'];
function B(n, t) {
  let s = t.rawDefinition(n).contains.find(
          c => k.includes(c == null ? void 0 : c.begin)),
      i = n.inherit(s, {begin: /hbs`/});
  return i.starts.subLanguage = 'glimmer', i
}
var r = {
  begin: /<template>/,
  end: /<\/template>/,
  isTrulyOpeningTag: (n, t) => {
    let a = n[0].length + n.index, s = n.input[a];
    if (s === '<' || s === ',') {
      t.ignoreMatch();
      return
    }
    let i;
    if ((i = n.input.substring(a).match(/^\s+extends\s+/)) && i.index === 0) {
      t.ignoreMatch();
      return
    }
  }
},
    z = {
      variants: [{begin: r.begin, 'on:begin': r.isTrulyOpeningTag, end: r.end}],
      subLanguage: 'glimmer',
      contains: [{begin: r.begin, end: r.end, skip: !0, contains: ['self']}]
    };
function b(n, t = 'javascript') {
  let a = n.getLanguage(t);
  if (!a) {
    console.warn(
        'JavaScript grammar not loaded. Cannot initialize glimmerJavascript.');
    return
  }
  return {
    name: 'glimmer-javascript', aliases: ['glimmer-js', 'gjs'], subLanguage: t,
        contains: [z, B(n, a)]
  }
}
var tn = o, J = b;
function an(n) {
  Z(n), h(n)
}
function sn(n) {
  let t = o(n);
  return h(n), t
}
function Z(n) {
  return n.registerLanguage('glimmer', o)
}
function h(n) {
  $(n)
}
function $(n) {
  let t = '_js-in-gjs', a = n.getLanguage('javascript');
  n.registerLanguage(t, s => a.rawDefinition(s)),
      n.unregisterLanguage('javascript'),
      n.registerLanguage('glimmer-javascript', s => {
        let i = J(s, t);
        return i.aliases.push('javascript', 'js', 'mjs', 'cjs', 'mjs'), i
      })
}
export {
  sn as externalSetup,
  tn as glimmer,
  J as glimmerJavascript,
  h as registerInjections,
  Z as registerLanguage,
  an as setup
};
1 Like

Clear your cache and try again. I believe it should be enabled for all themes.

5 Likes

Amazing!! Thanks so much for getting this through!

It does look like this changed a few CSS names here, I think. In particular the REPL’s prompt… and it’s always been strange that macros are styled the same as comments. But I think this is better:

the code for the above
struct Vec{T<:Real}
    x::T  # foo
    y::T  # bar
end

Base.:+(a::Vec, b::Vec) = Vec(a.x + b.x, a.y + b.y)

α = 1.23
v = Vec(1.0, α) + Vec(3.0, 4.0)

mode = :fast

if mode == :fast
    @info v
end
julia> struct Vec{T<:Real}
           x::T  # foo
           y::T  # bar
       end

julia> Base.:+(a::Vec, b::Vec) = Vec(a.x + b.x, a.y + b.y)

julia> α = 1.23
1.23

julia> v = Vec(1.0, α) + Vec(3.0, 4.0)
Vec{Float64}(4.0, 5.23)

julia> mode = :fast
:fast

julia> if mode == :fast
           @info v
       end
[ Info: Vec{Float64}(4.0, 5.23)
3 Likes