Improving Listed's code legibility in dark theme

It has been some time since I started writing on Listed. Although the development around the blogging platform seems to be slow-paced (which I feel is an understatement) and some note-taking services like Anytype also offer publication of notes, I have yet to find a compelling reason nor devote my time for migration.

That said, there is one thing I do want to see an improvement to.

The problem

Listed supports dark theme, which is not something I see in every blogging platforms I come across. However, I include a lot of code snippets, and switching to dark theme meant significant parts of the post become less legible.

Light theme Dark theme
A code snippet in light theme A code snippet in dark theme. Most notably, the equal sign in dark colour is less visible because its color did not change between themes.

Finding the code-highlighting library

I have tried reading the code of the blogging platform before, so I thought I might as well do the same thing again. With git clone, the repository was made ready.

[lyuk98@framework:~]$ git clone https://github.com/standardnotes/listed.git
[lyuk98@framework:~]$ cd listed/
[lyuk98@framework:~/listed]$ git switch --detach d7e82ea3725148d1dbc5960aa147b1aa4da8a215
[lyuk98@framework:~/listed]$ code .

My first theory was that Listed depends on a code-highlighting library. To find if there is any, I first searched for anything about highlight, which led me to yarn.lock that contains dependencies of interest.

"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.3":
  version "7.10.3"
  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.3.tgz#324bcfd8d35cd3d47dae18cde63d752086435e9a"
  integrity sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==
  dependencies:
    "@babel/highlight" "^7.10.3"

"@babel/code-frame@^7.10.4":
  version "7.10.4"
  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
  integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
  dependencies:
    "@babel/highlight" "^7.10.4"

It seemed like @babel/code-frame is what makes the code colourful. However, just to be sure, I read the raw response of my blog post and located a code snippet.

[lyuk98@framework:~]$ curl https://lyuk98.com/66674/building-obscure-packages-with-nix | less
<div class="highlight"><pre class="highlight nix"><code><span class="c"># Zen Browser</span>
<span class="nv">zen-browser</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:0xc000022070/zen-browser-flake"</span><span class="p">;</span>
  <span class="nv">inputs</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">nixpkgs</span><span class="o">.</span><span class="nv">follows</span> <span class="o">=</span> <span class="s2">"nixpkgs"</span><span class="p">;</span>
    <span class="nv">home-manager</span><span class="o">.</span><span class="nv">follows</span> <span class="o">=</span> <span class="s2">"home-manager"</span><span class="p">;</span>
  <span class="p">};</span>
<span class="p">};</span>
</code></pre></div>

Unlike my assumption, the response was already processed in some way, containing class values that were cryptic to the uninformed like me. Perhaps the highlighting is done by something else.

In the end, even if I find multiple possible fixes, the only one I could perform would be to create custom CSS styles. As such, I aimed to find the part that themes the <span> elements.

The stylesheet containing the theme definition seemed to be generated on the fly, especially given that the code in question is over 2,000 lines.

Style Editor section of the Web Developer Tools. A stylesheet named "application-75392...bae5d8c04dfa.css" is selected, partially showing its content from lines 2364 to 2376.Style Editor section of the Web Developer Tools. A stylesheet named "application-75392...bae5d8c04dfa.css" is selected, partially showing its content from lines 2364 to 2376.

I was unsure where it is created, but by pure luck, I was able to locate the part in question within Listed's code.

/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
 * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_self
 */

@import "stylekit";
@import "rouge";

#main-container {
  display: flex;
  min-height: 100%;
  flex-direction: column;
}

#content-container {
  height: 100%;
  flex: 1;
}

StyleKit, which seemed to be an archived project by Standard Notes, did not contain something interesting. However, Rouge, a "code highlighter", did.

La logithèque rouge

As I (hopefully correctly) found what does the code highlighting, I moved on to the next step. My guess was that since Listed depends on an old version of the library (3.26.1 versus 4.6.1 at the time of writing), a newer version would be prepared for the dark theme. To find out, I made a local copy of their code for reading.

[lyuk98@framework:~]$ git clone https://github.com/rouge-ruby/rouge.git
[lyuk98@framework:~]$ cd rouge/
[lyuk98@framework:~/rouge]$ git switch --detach 7a879833337f68fd358c350366db3f24cf441ed7
[lyuk98@framework:~/rouge]$ code .

Upon closer look, I found the cryptic class values in the library's code as well as its wiki.

Token name Token shortname Description
Text Any type of text data
Text.Whitespace w Specially highlighted whitespace
Error err Lexer errors
Other x Token for data not matched by a parser (e.g. HTML markup in PHP code)
Keyword k Any keyword
Keyword.Constant kc Keywords that are constants
Keyword.Declaration kd Keywords used for variable declaration (e.g. var in javascript)
Keyword.Namespace kn Keywords used for namespace declarations
Keyword.Pseudo kp Keywords that aren't really keywords
Keyword.Reserved kr Keywords which are reserved (such as end in Ruby)
Keyword.Type kt Keywords wich refer to a type id (such as int in C)
Name n Variable/function names
Name.Attribute na Attributes (in HTML for instance)
Name.Builtin nb Builtin names which are available in the global namespace
Name.Builtin.Pseudo bp Builtin names that are implicit (such as self in Ruby)
Name.Class nc For class declaration
Name.Constant no For constants
Name.Decorator nd For decorators in languages such as Python or Java
Name.Entity ni Token for entitites such as   in HTML
Name.Exception ne Exceptions and errors (e.g. ArgumentError in Ruby)
Name.Function nf Function names
Name.Property py Token for properties
Name.Label nl For label names
Name.Namespace nn Token for namespaces
Name.Other nx For other names
Name.Tag nt Tag mainly for markup such as XML or HTML
Name.Variable nv Token for variables
Name.Variable.Class vc Token for class variables (e.g. @@var in Ruby)
Name.Variable.Global vg For global variables (such as $LOAD_PATH in Ruby)
Name.Variable.Instance vi Token for instance variables (such as @var in Ruby)
Literal l Any literal (if not further defined)
Literal.Date ld Date literals
Literal.String s String literals
Literal.String.Backtick sb String enclosed in backticks
Literal.String.Char sc Token type for single characters
Literal.String.Doc sd Documentation strings (such as in Python)
Literal.String.Double s2 Double quoted strings
Literal.String.Escape se Escaped sequences in strings
Literal.String.Heredoc sh For "heredoc" strings (e.g. in Ruby)
Literal.String.Interpol si For interpoled part in strings (e.g. in Ruby)
Literal.String.Other sx Token type for any other strings (for example %q{foo} string constructs in Ruby)
Literal.String.Regex sr Regular expressions literals
Literal.String.Single s1 Single quoted strings
Literal.String.Symbol ss Symbols (such as :foo in Ruby)
Literal.Number m Any number literal (if not further defined)
Literal.Number.Float mf Float numbers
Literal.Number.Hex mh Hexadecimal numbers
Literal.Number.Integer mi Integer literals
Literal.Number.Integer.Long il Long interger literals
Literal.Number.Oct mo Octal literals
Literal.Number.Hex mx Hexadecimal literals
Literal.Number.Bin mb Binary literals
Operator o Operators (commonly +, -, /, *)
Operator.Word ow Word operators (e.g. and)
Punctuation p Punctuation which is not an operator
Comment c Single ligne comments
Comment.Multiline cm Mutliline comments
Comment.Preproc cp Preprocessor comments such as <% %> in ERb
Comment.Single c1 Comments that end at the end of the line
Comment.Special cs Special data in comments such as @license in Javadoc
Generic g Unstyled token
Generic.Deleted gd Token value as deleted
Generic.Emph ge Token value as emphasized
Generic.Error gr Token value as an error message
Generic.Heading gh Token value as a headline
Generic.Inserted gi Token value as inserted
Generic.Output go Marked as a program output
Generic.Prompt gp Marked as a command prompt
Generic.Strong gs Mark the token value as bold (for rst lexer)
Generic.Subheading gu Marked as a subheadline
Generic.Traceback gt Mark the token as a part of an error traceback
Generic.Lineno gl Line numbers

There were multiple themes with different colour values, making it slightly challenging to specify one to apply as a solution. Fortunately, I found a clue that suggests the GitHub theme is used by Listed.

<% require 'rouge' %>
<%= Rouge::Themes::Github.new.render %>

Even better, that specific theme gained support for dark theme with an update. What was now left to do was to write some CSS.

Applying the theme

Closely following the theme definition, I wrote a not-so-insignificant amount of CSS. Because the aforementioned update contains more than just adding new colours, I rewrote styles from scratch.

/* Light theme */
:root {
    /* Primer primitives */
    --P_RED_0: #ffebe9;
    --P_RED_5: #cf222e;
    --P_RED_7: #82071e;
    --P_ORANGE_6: #953800;
    --P_GREEN_0: #dafbe1;
    --P_GREEN_6: #116329;
    --P_BLUE_6: #0550ae;
    --P_BLUE_8: #0a3069;
    --P_PURPLE_5: #8250df;
    --P_GRAY_0: #f6f8fa;
    --P_GRAY_5: #6e7781;
    --P_GRAY_9: #24292f;

    /* Palettes */
    --palette-comment: var(--P_GRAY_5);
    --palette-constant: var(--P_BLUE_6);
    --palette-entity: var(--P_PURPLE_5);
    --palette-heading: var(--P_BLUE_6);
    --palette-keyword: var(--P_RED_5);
    --palette-string: var(--P_BLUE_8);
    --palette-tag: var(--P_GREEN_6);
    --palette-variable: var(--P_ORANGE_6);
    --palette-fgDefault: var(--P_GRAY_9);
    --palette-bgDefault: var(--P_GRAY_0);
    --palette-fgInserted: var(--P_GREEN_6);
    --palette-bgInserted: var(--P_GREEN_0);
    --palette-fgDeleted: var(--P_RED_7);
    --palette-bgDeleted: var(--P_RED_0);
    --palette-fgError: var(--P_GRAY_0);
    --palette-bgError: var(--P_RED_7);
}

@media (prefers-color-scheme: dark) {
    /* Dark theme */
    :root {
        /* Primer primitives */
        --P_RED_0: #ffdcd7;
        --P_RED_3: #ff7b72;
        --P_RED_7: #8e1519;
        --P_RED_8: #67060c;
        --P_ORANGE_2: #ffa657;
        --P_GREEN_0: #aff5b4;
        --P_GREEN_1: #7ee787;
        --P_GREEN_8: #033a16;
        --P_BLUE_1: #a5d6ff;
        --P_BLUE_2: #79c0ff;
        --P_BLUE_5: #1f6feb;
        --P_PURPLE_2: #d2a8ff;
        --P_GRAY_0: #f0f6fc;
        --P_GRAY_1: #c9d1d9;
        --P_GRAY_3: #8b949e;
        --P_GRAY_8: #161b22;

        /* Palettes */
        --palette-comment: var(--P_GRAY_3);
        --palette-constant: var(--P_BLUE_2);
        --palette-entity: var(--P_PURPLE_2);
        --palette-heading: var(--P_BLUE_5);
        --palette-keyword: var(--P_RED_3);
        --palette-string: var(--P_BLUE_1);
        --palette-tag: var(--P_GREEN_1);
        --palette-variable: var(--P_ORANGE_2);
        --palette-fgDefault: var(--P_GRAY_1);
        --palette-bgDefault: var(--P_GRAY_8);
        --palette-fgInserted: var(--P_GREEN_0);
        --palette-bgInserted: var(--P_GREEN_8);
        --palette-fgDeleted: var(--P_RED_0);
        --palette-bgDeleted: var(--P_RED_8);
        --palette-fgError: var(--P_GRAY_0);
        --palette-bgError: var(--P_RED_7);
    }
}

/* Text */
.highlight,
.highlight .w {
    color: var(--palette-fgDefault);
    background-color: var(--palette-bgDefault);
}

/* Keyword */
.highlight .k,
.highlight .kd,
.highlight .kn,
.highlight .kp,
.highlight .kr,
.highlight .kt,
.highlight .kv {
    color: var(--palette-keyword);
}

/* Generic.Error */
.highlight .gr {
    color: var(--palette-fgError);
}

/* Generic.Deleted */
.highlight .gd {
    color: var(--palette-fgDeleted);
    background-color: var(--palette-bgDeleted);
}

.highlight .nb, /* Name.Builtin */
.highlight .nc, /* Name.Class */
.highlight .no, /* Name.Constant */
.highlight .nn /* Name.Namespace */ {
    color: var(--palette-variable);
}

.highlight .sr, /* Literal.String.Regex */
.highlight .na, /* Name.Attribute */
.highlight .nt /* Name.Tag */ {
    color: var(--palette-tag);
}

/* Generic.Inserted */
.highlight .gi {
    color: var(--palette-fgInserted);
    background-color: var(--palette-bgInserted);
}

/* Generic.EmphStrong */
.highlight .ges {
    font-style: italic;
    font-weight: bold;
}

.highlight .kc, /* Keyword.Constant */
.highlight .l, /* Literal */
.highlight .ld,
.highlight .m,
.highlight .mb,
.highlight .mf,
.highlight .mh,
.highlight .mi,
.highlight .il,
.highlight .mo,
.highlight .mx,
.highlight .sb, /* Literal.String.Backtick */
.highlight .bp, /* Name.Builtin.Pseudo */
.highlight .ne, /* Name.Exception */
.highlight .nl, /* Name.Label */
.highlight .py, /* Name.Property */
.highlight .nv, /* Name.Variable */
.highlight .vc,
.highlight .vg,
.highlight .vi,
.highlight .vm,
.highlight .o, /* Operator */
.highlight .ow {
    color: var(--palette-constant);
}

.highlight .gh, /* Generic.Heading */
.highlight .gu /* Generic.Subheading */ {
    color: var(--palette-heading);
    font-weight: bold;
}

/* Literal.String */
.highlight .s,
.highlight .sa,
.highlight .sc,
.highlight .dl,
.highlight .sd,
.highlight .s2,
.highlight .se,
.highlight .sh,
.highlight .sx,
.highlight .s1,
.highlight .ss {
    color: var(--palette-string);
}

.highlight .nd, /* Name.Decorator */
.highlight .nf, /* Name.Function */
.highlight .fm {
    color: var(--palette-entity);
}

/* Error */
.highlight .err {
    color: var(--palette-fgError);
    background-color: var(--palette-bgError);
}

.highlight .c, /* Comment */
.highlight .ch,
.highlight .cd,
.highlight .cm,
.highlight .cp,
.highlight .cpf,
.highlight .c1,
.highlight .cs,
.highlight .gl, /* Generic.Lineno */
.highlight .gt /* Generic.Traceback */ {
    color: var(--palette-comment);
}

.highlight .ni,/* Name.Entity */
.highlight .si /* Literal.String.Interpol */ {
    color: var(--palette-fgDefault);
}

/* Generic.Emph */
.highlight .ge {
    color: var(--palette-fgDefault);
    font-style: italic;
}

/* Generic.Strong */
.highlight .gs {
    color: var(--palette-fgDefault);
    font-weight: bold;
}

/* Missing tokens */
.highlight .esc,
.highlight .x,
.highlight .n,
.highlight .nx,
.highlight .p,
.highlight .pi,
.highlight .g,
.highlight .go,
.highlight .gp {
    color: var(--palette-fgDefault);
}

When the change was applied, though, no token-specific colours were applied to any part of the code. Later on, I realised that some sort of sanitation process removed variable declarations from the stylesheet, rendering it pretty much useless.

/* Light theme */
:root {
    /* Primer primitives */













    /* Palettes */
















}

I was left with an unfavourable option, but I had no other choice; I replaced var() with the actual values.

/* Text */
.highlight,
.highlight .w {
    color: #24292f;
    background-color: #f6f8fa;
}

/* Keyword */
.highlight .k,
.highlight .kd,
.highlight .kn,
.highlight .kp,
.highlight .kr,
.highlight .kt,
.highlight .kv {
    color: #cf222e;
}

/* Generic.Error */
.highlight .gr {
    color: #f6f8fa;
}

/* Generic.Deleted */
.highlight .gd {
    color: #82071e;
    background-color: #ffebe9;
}

.highlight .nb, /* Name.Builtin */
.highlight .nc, /* Name.Class */
.highlight .no, /* Name.Constant */
.highlight .nn /* Name.Namespace */ {
    color: #953800;
}

.highlight .sr, /* Literal.String.Regex */
.highlight .na, /* Name.Attribute */
.highlight .nt /* Name.Tag */ {
    color: #116329;
}

/* Generic.Inserted */
.highlight .gi {
    color: #116329;
    background-color: #dafbe1;
}

/* Generic.EmphStrong */
.highlight .ges {
    font-style: italic;
    font-weight: bold;
}

.highlight .kc, /* Keyword.Constant */
.highlight .l, /* Literal */
.highlight .ld,
.highlight .m,
.highlight .mb,
.highlight .mf,
.highlight .mh,
.highlight .mi,
.highlight .il,
.highlight .mo,
.highlight .mx,
.highlight .sb, /* Literal.String.Backtick */
.highlight .bp, /* Name.Builtin.Pseudo */
.highlight .ne, /* Name.Exception */
.highlight .nl, /* Name.Label */
.highlight .py, /* Name.Property */
.highlight .nv, /* Name.Variable */
.highlight .vc,
.highlight .vg,
.highlight .vi,
.highlight .vm,
.highlight .o, /* Operator */
.highlight .ow {
    color: #0550ae;
}

.highlight .gh, /* Generic.Heading */
.highlight .gu /* Generic.Subheading */ {
    color: #0550ae;
    font-weight: bold;
}

/* Literal.String */
.highlight .s,
.highlight .sa,
.highlight .sc,
.highlight .dl,
.highlight .sd,
.highlight .s2,
.highlight .se,
.highlight .sh,
.highlight .sx,
.highlight .s1,
.highlight .ss {
    color: #0a3069;
}

.highlight .nd, /* Name.Decorator */
.highlight .nf, /* Name.Function */
.highlight .fm {
    color: #8250df;
}

/* Error */
.highlight .err {
    color: #f6f8fa;
    background-color: #82071e;
}

.highlight .c, /* Comment */
.highlight .ch,
.highlight .cd,
.highlight .cm,
.highlight .cp,
.highlight .cpf,
.highlight .c1,
.highlight .cs,
.highlight .gl, /* Generic.Lineno */
.highlight .gt /* Generic.Traceback */ {
    color: #6e7781;
}

.highlight .ni,/* Name.Entity */
.highlight .si /* Literal.String.Interpol */ {
    color: #24292f;
}

/* Generic.Emph */
.highlight .ge {
    color: #24292f;
    font-style: italic;
}

/* Generic.Strong */
.highlight .gs {
    color: #24292f;
    font-weight: bold;
}

/* Missing tokens */
.highlight .esc,
.highlight .x,
.highlight .n,
.highlight .nx,
.highlight .p,
.highlight .pi,
.highlight .g,
.highlight .go,
.highlight .gp {
    color: #24292f;
}

@media (prefers-color-scheme: dark) {
    /* Text */
    .highlight,
    .highlight .w {
        color: #c9d1d9;
        background-color: #161b22;
    }

    /* Keyword */
    .highlight .k,
    .highlight .kd,
    .highlight .kn,
    .highlight .kp,
    .highlight .kr,
    .highlight .kt,
    .highlight .kv {
        color: #ff7b72;
    }

    /* Generic.Error */
    .highlight .gr {
        color: #f0f6fc;
    }

    /* Generic.Deleted */
    .highlight .gd {
        color: #ffdcd7;
        background-color: #67060c;
    }

    .highlight .nb, /* Name.Builtin */
    .highlight .nc, /* Name.Class */
    .highlight .no, /* Name.Constant */
    .highlight .nn /* Name.Namespace */ {
        color: #ffa657;
    }

    .highlight .sr, /* Literal.String.Regex */
    .highlight .na, /* Name.Attribute */
    .highlight .nt /* Name.Tag */ {
        color: #7ee787;
    }

    /* Generic.Inserted */
    .highlight .gi {
        color: #aff5b4;
        background-color: #033a16;
    }

    /* Generic.EmphStrong */
    .highlight .ges {
        font-style: italic;
        font-weight: bold;
    }

    .highlight .kc, /* Keyword.Constant */
    .highlight .l, /* Literal */
    .highlight .ld,
    .highlight .m,
    .highlight .mb,
    .highlight .mf,
    .highlight .mh,
    .highlight .mi,
    .highlight .il,
    .highlight .mo,
    .highlight .mx,
    .highlight .sb, /* Literal.String.Backtick */
    .highlight .bp, /* Name.Builtin.Pseudo */
    .highlight .ne, /* Name.Exception */
    .highlight .nl, /* Name.Label */
    .highlight .py, /* Name.Property */
    .highlight .nv, /* Name.Variable */
    .highlight .vc,
    .highlight .vg,
    .highlight .vi,
    .highlight .vm,
    .highlight .o, /* Operator */
    .highlight .ow {
        color: #79c0ff;
    }

    .highlight .gh, /* Generic.Heading */
    .highlight .gu /* Generic.Subheading */ {
        color: #1f6feb;
        font-weight: bold;
    }

    /* Literal.String */
    .highlight .s,
    .highlight .sa,
    .highlight .sc,
    .highlight .dl,
    .highlight .sd,
    .highlight .s2,
    .highlight .se,
    .highlight .sh,
    .highlight .sx,
    .highlight .s1,
    .highlight .ss {
        color: #a5d6ff;
    }

    .highlight .nd, /* Name.Decorator */
    .highlight .nf, /* Name.Function */
    .highlight .fm {
        color: #d2a8ff;
    }

    /* Error */
    .highlight .err {
        color: #f0f6fc;
        background-color: #8e1519;
    }

    .highlight .c, /* Comment */
    .highlight .ch,
    .highlight .cd,
    .highlight .cm,
    .highlight .cp,
    .highlight .cpf,
    .highlight .c1,
    .highlight .cs,
    .highlight .gl, /* Generic.Lineno */
    .highlight .gt /* Generic.Traceback */ {
        color: #8b949e;
    }

    .highlight .ni,/* Name.Entity */
    .highlight .si /* Literal.String.Interpol */ {
        color: #c9d1d9;
    }

    /* Generic.Emph */
    .highlight .ge {
        color: #c9d1d9;
        font-style: italic;
    }

    /* Generic.Strong */
    .highlight .gs {
        color: #c9d1d9;
        font-weight: bold;
    }

    /* Missing tokens */
    .highlight .esc,
    .highlight .x,
    .highlight .n,
    .highlight .nx,
    .highlight .p,
    .highlight .pi,
    .highlight .g,
    .highlight .go,
    .highlight .gp {
        color: #c9d1d9;
    }
}

The result

The change was definitely noticeable. I could finally read code more comfortably.

Light theme Dark theme
A code snippet in light theme A code snippet in dark theme

It was one less reason for switching the note-taking service. However, it was not a proper solution; the developers could implement a much more permanent one.

Anyway, I was glad that this problem is finally over with.

More from 이영욱
All posts