5
\$\begingroup\$

I've written some SASS code to generate a Pygments syntax highlighter style sheet (which are, by extension, compatible with Rouge). Such a stylesheet is normally generated by the pygmentize command-line tool from predefined styles defined in Python classes, the code below reflects the Pygments token hierarchy, recording the relationship to the CSS classes used so we can output those classes.

The idea is that you define a nested map to set any of a foreground, background or border color, and if italics, bold or underlying should be enabled. For example, encoding the friendly style packaged with Pygments, becomes:

// Pygments "friendly" theme
$highlight-style: (
  comment: (color: #60a0b0, italic: true) (
    preproc: (#007020),
    special: (color: #60a0b0, bgcolor: #fff0f0),
  ),
  error: (border: #ff0000),
  generic: null (
    deleted: #a00000,
    error: #ff0000,
    heading: (color: #000080, bold: true),
    inserted: #00A000,
    output: #808080,
    prompt: (color: #c65d09, bold: true),
    subheading: (color: #800080, bold: true),
    traceback: #0040D0,
  ),
  highlight: (bgcolor: #ffffcc),
  keyword: (color: #007020, bold: true) (
    pseudo: #007020,
    type: #902000,
  ),
  literal: null (
    number: #40a070,  // with float, hex, integer, integer.long and oct subtypes
    string: #4070a0 (
      // backtick, char, double, heredoc, single all inheriting
      doc: (color: #4070a0, italic: true),
      escape: (color: #4070a0, bold: true),
      interpol: (color: #70a0d0, italic: true),
      other: #c65d09,
      regex: #235388,
      symbol: #517918,
    ),
  ),
  name: null (
    attribute: #4070a0,
    builtin: #007020,
    class: (color: #0e84b5, bold: true),
    constant: #60add5,
    decorator: (color: #555555, bold: true),
    entity: (color: #d55537, bold: true),
    exception: #007020,
    function: #06287e,
    label: (color: #002070, bold: true),
    namespace: (color: #0e84b5, bold: true),
    tag: (color: #062873, bold: true),
    variable: #bb60d5,  // with class, global and instance subtypes same
  ),
  operator: #666666 (
    word: (color: #007020, bold: true),
  ),
  text: null (
    whitespace: #bbbbbb,
  ),
);

The Python model is a little more sophisticated in that the 6 different properties are individually inherited; you could set a color for String then only add that String.Escape should use italics in addition. This then requires support for not italic, to disable the boolean options again. I opted for not making my version as advanced, you inherit all properties or define the set anew for child tokens.

This produces:

/* Pygments / rouge source code highlight styles */
.highlight .c { color: #60a0b0; font-style: italic; /* comment */ }
.highlight .ch { color: #60a0b0; font-style: italic; /* comment.hashbang */ }
.highlight .cm { color: #60a0b0; font-style: italic; /* comment.multiline */ }
.highlight .cp { color: #007020; /* comment.preproc */ }
.highlight .cpf { color: #60a0b0; font-style: italic; /* comment.preprocfile */ }
.highlight .c1 { color: #60a0b0; font-style: italic; /* comment.single */ }
.highlight .cs { color: #60a0b0; background-color: #fff0f0; /* comment.special */ }
.highlight .err { border: 1px solid #ff0000; /* error */ }
.highlight .gd { color: #a00000; /* generic.deleted */ }
.highlight .ge { font-style: italic; /* generic.emph */ }
.highlight .gr { color: #ff0000; /* generic.error */ }
.highlight .gh { color: #000080; font-weight: bold; /* generic.heading */ }
.highlight .gi { color: #00A000; /* generic.inserted */ }
.highlight .go { color: #808080; /* generic.output */ }
.highlight .gp { color: #c65d09; font-weight: bold; /* generic.prompt */ }
.highlight .gs { font-weight: bold; /* generic.strong */ }
.highlight .gu { color: #800080; font-weight: bold; /* generic.subheading */ }
.highlight .gt { color: #0040D0; /* generic.traceback */ }
.highlight .hll { background-color: #ffffcc; /* highlight */ }
.highlight .k { color: #007020; font-weight: bold; /* keyword */ }
.highlight .kc { color: #007020; font-weight: bold; /* keyword.constant */ }
.highlight .kd { color: #007020; font-weight: bold; /* keyword.declaration */ }
.highlight .kn { color: #007020; font-weight: bold; /* keyword.namespace */ }
.highlight .kp { color: #007020; /* keyword.pseudo */ }
.highlight .kr { color: #007020; font-weight: bold; /* keyword.reserved */ }
.highlight .kt { color: #902000; /* keyword.type */ }
.highlight .m { color: #40a070; /* literal.number */ }
.highlight .mb { color: #40a070; /* literal.number.bin */ }
.highlight .mf { color: #40a070; /* literal.number.float */ }
.highlight .mh { color: #40a070; /* literal.number.hex */ }
.highlight .mi { color: #40a070; /* literal.number.integer */ }
.highlight .il { color: #40a070; /* literal.number.integer.long */ }
.highlight .mo { color: #40a070; /* literal.number.oct */ }
.highlight .s { color: #4070a0; /* literal.string */ }
.highlight .sa { color: #4070a0; /* literal.string.affix */ }
.highlight .sb { color: #4070a0; /* literal.string.backtick */ }
.highlight .sc { color: #4070a0; /* literal.string.char */ }
.highlight .dl { color: #4070a0; /* literal.string.delimiter */ }
.highlight .sd { color: #4070a0; font-style: italic; /* literal.string.doc */ }
.highlight .s2 { color: #4070a0; /* literal.string.double */ }
.highlight .se { color: #4070a0; font-weight: bold; /* literal.string.escape */ }
.highlight .sh { color: #4070a0; /* literal.string.heredoc */ }
.highlight .si { color: #70a0d0; font-style: italic; /* literal.string.interpol */ }
.highlight .sx { color: #c65d09; /* literal.string.other */ }
.highlight .sr { color: #235388; /* literal.string.regex */ }
.highlight .s1 { color: #4070a0; /* literal.string.single */ }
.highlight .ss { color: #517918; /* literal.string.symbol */ }
.highlight .na { color: #4070a0; /* name.attribute */ }
.highlight .nb { color: #007020; /* name.builtin */ }
.highlight .bp { color: #007020; /* name.builtin.pseudo */ }
.highlight .nc { color: #0e84b5; font-weight: bold; /* name.class */ }
.highlight .no { color: #60add5; /* name.constant */ }
.highlight .nd { color: #555555; font-weight: bold; /* name.decorator */ }
.highlight .ni { color: #d55537; font-weight: bold; /* name.entity */ }
.highlight .ne { color: #007020; /* name.exception */ }
.highlight .nf { color: #06287e; /* name.function */ }
.highlight .fm { color: #06287e; /* name.function.magic */ }
.highlight .nl { color: #002070; font-weight: bold; /* name.label */ }
.highlight .nn { color: #0e84b5; font-weight: bold; /* name.namespace */ }
.highlight .nt { color: #062873; font-weight: bold; /* name.tag */ }
.highlight .nv { color: #bb60d5; /* name.variable */ }
.highlight .vc { color: #bb60d5; /* name.variable.class */ }
.highlight .vg { color: #bb60d5; /* name.variable.global */ }
.highlight .vi { color: #bb60d5; /* name.variable.instance */ }
.highlight .vm { color: #bb60d5; /* name.variable.magic */ }
.highlight .o { color: #666666; /* operator */ }
.highlight .ow { color: #007020; font-weight: bold; /* operator.word */ }
.highlight .w { color: #bbbbbb; /* text.whitespace */ }

The idea is that you can now use SASS color functions to generate a style from a base color scheme.

This is the SASS partial that produces the output:

// Pygment / Rouge style generation

$highlight-selector: ".highlight";

// Define styles for the class hierarchy as a nested map;
// values are argument lists for the pygments-rule mixin
// Nested elements take the parent style if not set to null
$highlight-style: () !default;
$highlight-style-defaults: (
    generic: null (
        emph: (italic: true),
        strong: (bold: true),
    ),
);
$highlight-style-classes: (
    comment: 'c' (
        hashbang: 'ch',
        multiline: 'cm',
        preproc: 'cp',
        preprocfile: 'cpf',
        single: 'c1',
        special: 'cs',
    ),
    escape: 'esc',
    error: 'err',
    generic: null (
        deleted: 'gd',
        emph: 'ge',
        error: 'gr',
        heading: 'gh',
        inserted: 'gi',
        output: 'go',
        prompt: 'gp',
        strong: 'gs',
        subheading: 'gu',
        traceback: 'gt',
    ),
    highlight: 'hll',
    keyword: 'k' (
        constant: 'kc',
        declaration: 'kd',
        namespace: 'kn',
        pseudo: 'kp',
        reserved: 'kr',
        type: 'kt'
    ),
    literal: 'l' (
        date: 'ld',
        number: 'm' (
            bin: 'mb',
            float: 'mf',
            hex: 'mh',
            integer: 'mi' (
                long: 'il'
            ),
            oct: 'mo',
        ),
        string: 's' (
            affix: 'sa',
            backtick: 'sb',
            char: 'sc',
            delimiter: 'dl',
            doc: 'sd',
            double: 's2',
            escape: 'se',
            heredoc: 'sh',
            interpol: 'si',
            other: 'sx',
            regex: 'sr',
            single: 's1',
            symbol: 'ss',
        ),
    ),
    name: 'n' (
        attribute: 'na',
        builtin: 'nb' (
            pseudo: 'bp',
        ),
        class: 'nc',
        constant: 'no',
        decorator: 'nd',
        entity: 'ni',
        exception: 'ne',
        function: 'nf' (
            magic: 'fm'
        ),
        label: 'nl',
        namespace: 'nn',
        other: 'nx',
        property: 'py',
        tag: 'nt',
        variable: 'nv' (
            class: 'vc',
            global: 'vg',
            instance: 'vi',
            magic: 'vm',
        ),
    ),
    operator: 'o' (
        word: 'ow',
    ),
    other: 'x',
    punctuation: 'p',
    text: null (
        whitespace: 'w',
    ),
);

@function highlight-map-deep-get($map, $keys...) {
    $value: null;
    @each $key in $keys {
        $entry: map-get($map, $key);
        @if $entry != null {
            $value: if(type-of($entry) == list, nth($entry, 1), $entry);
            $map: if(type-of($entry) == list and length($entry) > 1, nth($entry, 2), null) or ();
        } @else {
            @return null;
        }
    }
    @return $value;
}

@function highlight-map-merge($parent-map, $child-map) {
    $result: $parent-map;
    @each $key, $value in $child-map {
        $resultvalue: map-get($result, $key);
        @if not map-has-key($result, $key) {
            $result: map-merge($result, ($key: $value));
        } @else {
            $resultvalue1: if(type-of($resultvalue) == list, nth($resultvalue, 1), $resultvalue);
            $value1: if(type-of($value) == list, nth($value, 1), $value) or $resultvalue1;
            $resultmap: if(type-of($resultvalue) == list and length($resultvalue) > 1, nth($resultvalue, 2), null);
            $valuemap: if(type-of($value) == list and length($value) > 1, nth($value, 2), null);
            @if $resultmap and $valuemap {
                $valuemap: highlight-map-merge($resultmap or (), $valuemap or ());
            };
            $result: map-merge($result, ($key: $value1 $valuemap));
        }
    }
    @return $result;
}

@function to-dotted-string($list) {
  $result: null;

  @for $i from 1 through length($list) {
    $e: nth($list, $i);
    $result: if($i != length($list), "#{$result}#{$e}.", "#{$result}#{$e}");
  }

  @return $result;
}


@mixin highlight-rule($color: null, $bold: false, $italic: false, $underline: false, $bgcolor: null, $border: null) {
    @if $color     { color: $color; }
    @if $bold      { font-weight: bold; }
    @if $italic    { font-style: italic; }
    @if $underline { text-decoration: underline; }
    @if $bgcolor   { background-color: $bgcolor; }
    @if $border    { border: 1px solid $border; }
}

@mixin highlight-class($map, $values, $path: (), $parentvalue: null) {
    @each $key, $value in $map {
        $classname: nth($value, 1);
        $fullpath: append($path, $key);
        $styleargs: null;
        @if $classname != null {
            $styleargs: highlight-map-deep-get($values, $fullpath...) or $parentvalue or ();
            @if $styleargs != null and length($styleargs) > 0 {
                .#{$classname} {
                    @include highlight-rule($styleargs...);
                    /* #{to-dotted-string($fullpath)} */
                }
            }
        }
        @if length($value) > 1 {
            // recursion to subelelements
            @include highlight-class(nth($value, 2), $values, $fullpath, $styleargs);
        };
    };
};

/* Pygments / rouge source code highlight styles */
#{$highlight-selector} {
    @include highlight-class(
        $highlight-style-classes,
        highlight-map-merge($highlight-style-defaults, $highlight-style)
    );
};

As this is my first piece of SASS mixin code, I'd like to have feedback on style and techniques used.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.