Skip to content

How to style specific nested tokens? #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
adamwathan opened this issue Aug 5, 2019 · 8 comments
Closed

How to style specific nested tokens? #204

adamwathan opened this issue Aug 5, 2019 · 8 comments

Comments

@adamwathan
Copy link

Is your feature request related to a problem? Please describe.

The built-in styles include nested token styles like this:

https://github.com/conorhastings/react-syntax-highlighter/blob/master/src/styles/prism/ghcolors.js#L163

However as far as I can tell they don't actually seem to work.

Describe the solution you'd like

It would be great if there was some way to target nested tokens like you can when writing a CSS theme for Prism/Highlight.

Describe alternatives you've considered

Tried a bunch of syntaxes to see if anything worked:

<SyntaxHighlighter
  style={{
    '.token.atrule .token.rule': { background: 'pink' }
    'atrule rule': { background: 'pink' }
    'atrule.rule': { background: 'pink' }
  }}
  {/* ... */}
/>
@karlhorky
Copy link
Contributor

Just ran into this today, trying to style interpolation-punctuation.

Simple reproduction here:

Screen Shot 2020-08-24 at 14 21 28

Sandbox: https://codesandbox.io/s/react-syntax-highlighter-forked-kjnk7?file=/src/index.js:265-559

@karlhorky
Copy link
Contributor

karlhorky commented Aug 24, 2020

@simmerer I would like to try my hand at doing a PR for this.

I've narrowed it down to this part of the create-element.js file:

export function createStyleObject(classNames, elementStyle = {}, stylesheet) {
return classNames.reduce((styleObject, className) => {
return { ...styleObject, ...stylesheet[className] };
}, elementStyle);
}

For example, if the classNames variable looks like this, which it does for the case of interpolation-punctuation:

["token", "template-string", "token", "interpolation", "token", "interpolation-punctuation", "punctuation"]

I can think of two ways to solve this:

  1. Make the order of the classNames array go from less specific to more specific (in terms of Prism.js hierarchy)
  2. Sort the keys of the stylesheet by CSS specificity and iterate over them instead of the classNames

Edit: After thinking a bit more about this, I guess the second solution would be the best here, since solution 1 means modifying the classNames array, which comes already from the astGenerator (eg. refractor).

@karlhorky
Copy link
Contributor

I will come up with a prototype of the specificity solution above (solution 2).

@karlhorky
Copy link
Contributor

karlhorky commented Aug 24, 2020

Ok so using specificity, we can do this solution (working, but not yet optimized):

Screen Shot 2020-08-24 at 15 09 51

import React from 'react';
+import { compare } from 'specificity';

export function createStyleObject(classNames, elementStyle = {}, stylesheet) {
+  const sortedStylesheet = Object.entries(stylesheet).sort(([selectorA], [selectorB]) => {
+    return compare(selectorA, selectorB)
+  });
-  return classNames.reduce((styleObject, className) => {
-    return { ...styleObject, ...stylesheet[className] };
+  return sortedStylesheet.reduce((styleObject, [selector, styles]) => {
+    return { ...styleObject, ...(classNames.includes(selector) && styles) };
  }, elementStyle);
}

Edit: Or, a different code style, potentially easier to follow:

import React from 'react';
+import { compare } from 'specificity';

export function createStyleObject(classNames, elementStyle = {}, stylesheet) {
-  return classNames.reduce((styleObject, className) => {
-    return { ...styleObject, ...stylesheet[className] };
-  }, elementStyle);
+  return Object.entries(stylesheet)
+    .filter(([selector]) => classNames.includes(selector))
+    .sort(([selectorA], [selectorB]) => {
+      return compare(selectorA, selectorB);
+    })
+    .reduce((styleObject, [, styles]) => {
+      return { ...styleObject, ...styles };
+    }, elementStyle);
}

@simmerer is it acceptable to add this dependency? I'm guessing probably not optimal...

Another way would be to manually count specificity without the library.

@karlhorky
Copy link
Contributor

karlhorky commented Aug 24, 2020

Hmm... I guess this solution is just working by coincidence actually, since classNames is this:

["token", "template-string", "token", "interpolation", "token", "interpolation-punctuation", "punctuation"]

So the styles are like this (same specificity):

{
  punctuation: {
    color: '#ff0000',
  },
  'interpolation-punctuation': {
    color: '#00ff00',
  },
}

@karlhorky
Copy link
Contributor

karlhorky commented Aug 24, 2020

Ok, found a zero-dependency alternative, which:

  1. Splits up the selectors in the stylesheet by .s into an array
  2. Checks whether this array is a subset of the classNames array
  3. Adds styles related to regular selectors to the beginning of the styles to be less specific
  4. Adds styles related to class selectors to the end of the styles to be more specific

Feedback on this approach?

+const classSelectorClassNames = {};
+function classSelectorToClassNames(selector) {
+  if (!selector.includes(".")) return null;
+  if (!classSelectorClassNames[selector]) {
+    classSelectorClassNames[selector] = selector.split(".").filter(Boolean);
+  }
+  return classSelectorClassNames[selector];
+}
+
+// Is array1 a subset of array2
+function isSubset(array1, array2) {
+  if (!array1 || !array2) return false;
+  if (!Array.isArray(array1) || !Array.isArray(array2)) return false;
+  const intersection = array1.filter((element) => array2.includes(element));
+  return intersection.length === array1.length;
+}

export function createStyleObject(classNames, elementStyle = {}, stylesheet) {
-  return classNames.reduce((styleObject, className) => {
-    return { ...styleObject, ...stylesheet[className] };
-  }, elementStyle);
+  const stylesheetStyles = Object.entries(stylesheet).reduce(
+    (newStyleObject, [selector, styles]) => {
+      if (classNames.includes(selector)) {
+        return { ...styles, ...newStyleObject };
+      } else if (isSubset(classSelectorToClassNames(selector), classNames)) {
+        return { ...newStyleObject, ...styles };
+      }
+      return newStyleObject;
+    },
+    {}
+  );
+  return { ...elementStyle, ...stylesheetStyles };
}

@karlhorky
Copy link
Contributor

Opened a PR with this approach at #305 - of course open for feedback and changing the approach!

@karlhorky
Copy link
Contributor

@adamwathan a solution from #305 has now been merged and is available in react-syntax-highlighter@15.0.1!

If it works for you, maybe this issue can be closed :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants