3

I'm working on a project in which a user can select colors from a color input and create their own theme dynamically using CSS variables. I'd like the user to be able to download the entire CSS file with the values they selected.

My issue: The CSS file downloaded doesn't display the actual color values, but shows the variable name.

NOT WANTED

pre[class*="language-"] {
  background: var(--block-background);
}

instead of

WANTED OUTPUT

pre[class*="language-"] {
  background: #0D2831;
}

I know I can get CSS property values by doing the following.

const styles = getComputedStyle(document.documentElement)
const value = String(styles.getPropertyValue('--block-background')).trim()

I figured that I would create a function that loops through all my CSS variables and grabs the corresponding property values and then adds them to a new stylesheet for the user to download, but I got lost along the way. I currently have two CSS files, a main.css and a prism.css. The main.css file holds the page styling and all CSS variables within the root. The prism.css file contains the theme in which I want the user to be able to download.

I'm trying to find a way to create a new stylesheet that contains everything within the prism.css file but has the actual color hex code instead of the CSS variable name as a value to the given CSS property.

Index.js

import { colors } from './colorHelper'

const inputs = [].slice.call(document.querySelectorAll('input[type="color"]'));

const handleThemeUpdate = (colors) => {
  const root = document.querySelector(':root');
  const keys = Object.keys(colors);
  keys.forEach(key => {
    root.style.setProperty(key, colors[key]);
  });
}

inputs.forEach((input) => {
  input.addEventListener('change', (e) => {
    e.preventDefault()
    const cssPropName = `--${e.target.id}`;
    document.styleSheets[2].cssRules[3].style.setProperty(cssPropName, e.target.value)
    handleThemeUpdate({
      [cssPropName]: e.target.value
    });
    console.log(`${cssPropName} is now ${e.target.value}`)
  });
});


const cssRules = document.styleSheets[2].cssRules;
for (var i = 0; i < cssRules.length; i++) {
  // Finds css variable names
  const regexp = /(?:var\(--)[a-zA-z\-]*(?:\))/

  let cssVariables = cssRules[i].cssText.matchAll(regexp)
  cssVariables = Array.from(cssVariables).join()

  console.log(cssVariables)
}

colorHelper.js

const colorSelect = {
  'Line Highlights': {
    'highlight-background': '#F7EBC6',
    'highlight-accent': '#F7D87C'
  },
  'Inline Code': {
    'inline-code-color': '#DB4C69',
    'inline-code-background': '#F9F2F4'
  },
  'Code Blocks': {
    'block-background': '#0D2831',
    'base-color': '#5C6E74',
    'selected-color': '#b3d4fc'
  },
  'Tokens': {
    'comment-color': '#93A1A1',
    'punctuation-color': '#999999',
    'property-color': '#990055',
    'selector-color': '#669900',
    'operator-color': '#a67f59',
    'operator-background': '#FFFFFF',
    'variable-color': '#ee9900',
    'function-color': '#DD4A68',
    'keyword-color': '#0077aa'
  }
}

const colorNames = []
const colors = {}

Object.keys(colorSelect).map(key => {
  const group = colorSelect[key]
  Object.keys(group).map(color => {
    colorNames.push(color)
    colors[color] = group[color]
  })
})

export { colorSelect, colorNames, colors }

prism.css

pre[class*="language-"],
code[class*="language-"] {
  color: var(--base-color);
  font-size: 13px;
  text-shadow: none;
  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
  direction: ltr;
  text-align: left;
  white-space: pre;
  word-spacing: normal;
  word-break: normal;
  line-height: 1.5;
  -moz-tab-size: 4;
  -o-tab-size: 4;
  tab-size: 4;
  -webkit-hyphens: none;
  -moz-hyphens: none;
  -ms-hyphens: none;
  hyphens: none;
}
pre[class*="language-"]::selection,
code[class*="language-"]::selection,
pre[class*="language-"]::mozselection,
code[class*="language-"]::mozselection {
  text-shadow: none;
  background: var(--selected-color);
}

@media print {
  pre[class*="language-"],
  code[class*="language-"] {
    text-shadow: none;
  }
}

pre[class*="language-"] {
  padding: 1em;
  margin: .5em 0;
  overflow: auto;
  background: var(--block-background);
}
:not(pre) > code[class*="language-"] {
  padding: .1em .3em;
  border-radius: .3em;
  color: var(--inline-code-color);
  background: var(--inline-code-background);
}

/* Tokens */

.namespace {
  opacity: .7;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
  color: var(--comment-color);
}
.token.punctuation {
  color: var(--punctuation-color);
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
  color: var(--property-color);
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
  color: var(--selector-color);
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
  color: var(--operator-color);
  background: var(--operator-background);
}
.token.atrule,
.token.attr-value,
.token.keyword {
  color: var(--keyword-color);
}
.token.function {
  color: var(--function-color);
}
.token.regex,
.token.important,
.token.variable {
  color: var(--variable-color);
}
.token.important,
.token.bold {
  font-weight: bold;
}
.token.italic {
  font-style: italic;
}
.token.entity {
  cursor: help;
}

/* Line highlighting */

pre[data-line] {
  position: relative;
}
pre[class*="language-"] > code[class*="language-"] {
  position: relative;
  z-index: 1;
}
.line-highlight {
  position: absolute;
  left: 0;
  right: 0;
  padding: inherit 0;
  margin-top: 1em;
  background: var(--highlight-background);
  box-shadow: inset 5px 0 0 var(--highlight-accent);
  z-index: 0;
  pointer-events: none;
  line-height: inherit;
  white-space: pre;
}

I have three stylesheets.

style.css holds the CSS variables in the root

normalize.css

prism.css contains the styles for syntax highlighting. This is the stylesheet I would like the user to download, but I would like to provide them with the actual hex values for each variable and not the variable name for the CSS property.

Stylesheet order in my HTML

 <link rel="stylesheet" type="text/css" href="./style.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css"
    integrity="sha256-WAgYcAck1C1/zEl5sBl5cfyhxtLgKGdpI3oKyJffVRI=" crossorigin="anonymous" />
  <link href="./themes/prism.css" rel="stylesheet" />

EDIT

I attempted to loop through the stylesheet and grab the CSS variable names, but some of them returned as an empty string.

This is what I did

const cssRules = document.styleSheets[2].cssRules;
for (var i = 0; i < cssRules.length; i++) {
  const regexp = /(?:var\(--)[a-zA-z\-]*(?:\))/

  let cssVariables = cssRules[i].cssText.matchAll(regexp)
  cssVariables = Array.from(cssVariables)

  console.log(cssVariables)
} 

This was the result in the console

var(--base-color) 
var(--selected-color) 
<empty string>
var(--block-background)
var(--inline-code-color)
<empty string>
var(--comment-color)
var(--punctuation-color)
var(--property-color) 
var(--selector-color)
var(--operator-color)
var(--keyword-color) 
var(--function-color)
var(--variable-color) 
<empty string> 
var(--highlight-background)

I then attempted to chain .replace() after the trim() but that didn't seem to work either.

Dave.Q
  • 369
  • 1
  • 6
  • 21
  • Have you tried doing a simple find and replace? – Trobol Dec 28 '19 at 01:03
  • I would loop through all CSS values that are variables and replace them with the value chosen, but how would I maintain everything from the original stylesheet while appending the new values? – Dave.Q Dec 28 '19 at 18:50
  • Your regex is invalid, I made one that might work better: https://regexr.com/4rgj2 – Trobol Dec 31 '19 at 22:55
  • @Dave.Q you "attempted to loop through the stylesheet"... can we see this stylesheet, please? – x00 Jan 07 '20 at 23:17
  • @x00 No problem. I've edited my question and included the stylesheet and other related information. – Dave.Q Jan 07 '20 at 23:36

2 Answers2

1

You can download the file as text then find and replace the variables.

For example:

var s = `pre[class*="language-"] {
  background: var(--block-background);
}`

const variables = {"block-background":"#0D2831"};

Object.keys(variables).forEach(key => {
  s = s.replace("var(--"+key+")", variables[key]); 
});

console.log(s);
Trobol
  • 1,210
  • 9
  • 12
  • I edited my original question with an attempt I made. Upon looping through the stylesheet to find a matching regex for the CSS variables, I was given an empty array for each. Any idea why that is? – Dave.Q Dec 31 '19 at 22:49
1
  1. You are getting empty strings from css rules that do not have var(--something) in them. Like

    @media print {
      pre[class*="language-"],
      code[class*="language-"] {
        text-shadow: none;
      }
    }
    

    which gives you the first empty string.

  2. You are missing var(--operator-background) because matchAll() actually doesn't do what you expect. It does

    returns an iterator of all results matching a string against a regular expression

    but the regular expression you have yields only one result. So you need to add g flag to it

    /(?:var\(--)[a-zA-z\-]*(?:\))/g

  3. mozselection... Hmm... Not sure, but shouldn't it be -moz-selection?

  4. The full loop for replacements can look like this:

     const updated_rules = [];
     for (var i = 0; i < cssRules.length; i++) {
        const regexp = /(?:var\(--)[a-zA-z\-]*(?:\))/g;
        let updated_rule = cssRules[i].cssText;
        let cssVariables = updated_rule.matchAll(regexp);
        cssVariables = Array.from(cssVariables).flat();
        for (const v of cssVariables) {
          updated_rule = updated_rule.replace(v, colors[v.slice(6, -1)]);
        }
        updated_rules.push(updated_rule);
      }
      console.log(updated_rules);
    

    It's an ugly code, and should be refactored, but...

  5. Why would you access css through document.styleSheets anyway? It's harder than just replacing strings in a css-file and for one thing, I'm not sure if you whould be able to access ::-moz-selection rule on Chrome, and in turn ::-webkit-selection on Firefox

x00
  • 13,643
  • 3
  • 16
  • 40
  • Thank you so much for the detailed answer. My question to the last point in your answer is how would I change strings in a CSS file? I'm just looking for a way for the user to choose their own colors and then download the stylesheet with his/her selected styles. I'm assuming replace the strings in the file and add the updated styles to a new style tag in the head? I'm a bit lost on this one. – Dave.Q Jan 08 '20 at 16:22
  • 1
    @Dave.Q just get prism.css strait from your server and replace variables. Like so: `async function get_css_and_replace_vars() { const res = await fetch("./prism.css"); const orig_css = await res.text(); let updated_css = orig_css; const regexp = /(?:var\(--)[a-zA-z\-]*(?:\))/g; let cssVars = orig_css.matchAll(regexp); cssVars = Array.from(cssVars).flat(); for (const v of cssVars) updated_css = updated_css.replace(v, colors[v.slice(6, -1)]); console.log(updated_css); ... }` What you do with original prims.css and with an updated version of it is up to you. – x00 Jan 08 '20 at 18:38
  • And yes, you can do exactly as you said. – x00 Jan 08 '20 at 18:39
  • I'm using Parcel to bundle everything and it's giving me an error because the `href` I've provided isn't found since I'm creating the style tag through JavaScript. `const createStyleSheet = () => { const css = document.createElement('style'); css.type = 'text/css' css.href = 'prism.css' css.innerHTML = get_css_and_replace_vars() const script = document.querySelector('script') script.parentNode.insertBefore(css, script) }`. My download button looks like this `Download CSS` – Dave.Q Jan 10 '20 at 13:25
  • @Dave.Q, sorry didn't really get what you mean. Is you issue with user downloading already updated css, or is it with creating a style tag. But as I see it now, you maybe have both :) ...why would you need to set href at all, if you use innerHTML. Maybe this will help: https://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript?answertab=active#tab-top If not, can you post it as another question with more details? – x00 Jan 10 '20 at 14:09
  • My issue is with downloading the updated CSS. I figured I would create a style tag and inject the updated styles. Then I would use the href on the new style element to correspond with that of the download button, but it didn't work as I was given an error stating that prism. CSS isn't available. I'm more than certain I'm going about this wrong or missing something. I returned the `updated_css` from the `get_css..` function and applied it to the innerHTML and figured I could just download the script tag. – Dave.Q Jan 10 '20 at 14:41
  • https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server – x00 Jan 10 '20 at 14:51