17

I'm trying to set up a service in python using pdfKit to create a pdf file from html files.

So basically I will send my element as string and expect the server to return a pdf version of it, but to be an accurate representation I also need to send a css file of the element.

How can I do this? Generate JSON / object with only the relevant style properties and selectors of an element and all its children. Respecting hierarchy and no duplicates. There are similar questions but they are outdated and tend to not consider children elements.

I was thinking maybe there is a way to create a new DOM from this element and then get the root css?

Preview
  • 35,317
  • 10
  • 92
  • 112
Mojimi
  • 2,561
  • 9
  • 52
  • 116
  • Have you looked into `window.getComputedStyle` https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle ? It returns an object with all CSS properties applied to an element. – elveti Dec 04 '18 at 12:34
  • @elveti Sure, but how do I get all children's style while respecting the hierarchy – Mojimi Dec 04 '18 at 12:36
  • @Mojimi the computed style does respect the hierarchy – Alnitak Dec 04 '18 at 12:38
  • https://stackoverflow.com/questions/754607/can-jquery-get-all-css-styles-associated-with-an-element – Pete Dec 04 '18 at 12:41
  • @Alnitak But does it gets all children styles too? – Mojimi Dec 04 '18 at 12:43
  • 2
    See the top answer Original (ES 2017) here: https://stackoverflow.com/questions/19784064/set-javascript-computed-style-from-one-element-to-another but AFAIK it doesn't apply children styles. You'd have to run the function again for each child I think – elveti Dec 04 '18 at 13:22
  • @Mojimi no, you have to ask for the style of each individual element, but those styles take into account those inherited from the parent. – Alnitak Dec 04 '18 at 15:24
  • @Alnitak exactly the point of my question, how to traverse all children, output a single css properties object while respecting the hierarchy because you will eventually overwrite properties and the overwriting needs to follow the tree order – Mojimi Dec 04 '18 at 15:33
  • @Mojimi right - you can't do that. You need to collect the computed properties of each element _from the bottom up_, and keep that set of properties associated with each individual element. – Alnitak Dec 04 '18 at 16:21
  • 2
    Create a loop, and the stop conditional will be an "id" you define in last child you want to get css property. – Yoarthur Dec 07 '18 at 23:33
  • Maybe not even a loop, but something recursive. Might be a goo use case for it – Jacob Dec 09 '18 at 20:35
  • What about media queries and such? Why not just include the CSS files that are referenced in the HTML? –  Dec 11 '18 at 19:08

2 Answers2

6

Here is something I came up with, basically pass the element you want to extract the styles of and ones of its children, and it will return you the stylesheet as a string. Open your console before running the snippet and you will see the output from the console.log.

Because I wanted to support the extraction of every element even those without a selector, I had to replace each element id by a unique uuid specifically generated for them in order to facilitate the styling of your output. The problem with this approach is in case you are using ids for styling or for user interaction, you are going to loose such functionality on concerned elements after calling extractCSS.

However, it is pretty trivial to use the oldId I'm passing to change back once your pdfKit process finished the generation. Simply call swapBackIds passing the elements returned by the function. You can see the difference of behavior if you uncomment the call in my snippet: the #root pink background would disappear because the styling targets an element id.

All in all, you need to:

  1. Call extractCSS with the element you want to extract
  2. Generate your pdf using res.stylesheet
  3. Call swapBackIds with res.elements

// Generate an unique id for your element
// From https://stackoverflow.com/a/2117523/2054072
function uuidv4 () {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

// Flatten an array
// https://stackoverflow.com/a/15030117/2054072
function flatten(arr) {
  return arr.reduce(function (flat, toFlatten) {
    return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
  }, []);
}

function recursiveExtract (element) {
  var id = uuidv4()
  var oldId = element.id

  var computed = window.getComputedStyle(element)
  var style = computed.cssText

  // Now that we get the style, we can swap the id
  element.setAttribute('id', id)

  // The children are not a real array but a NodeList, we need to convert them
  // so we can map over them easily
  var children = Array.prototype.slice.call(element.children)
  return [{ id: id, style: style, oldId: oldId }].concat(children.map(recursiveExtract))
}

function extractCSS (element) {
  if (!element) { return { elements: [], stylesheet: '' } }

  var raw = recursiveExtract(element)
  var flat = flatten(raw)
  
  return {
    elements: flat,
    stylesheet: flat.reduce(function (acc, cur) {
      var style = '#' + cur.id + ' {\n' + cur.style + '\n}\n\n'
      return acc + style
    }, '')
  }
}

var pdfElement = document.querySelector('#root')
var res = extractCSS(pdfElement)

console.log(res.stylesheet)

function swapBackIds (elements) {
  elements.forEach(function (e) {
    var element = document.getElementById(e.id)
    element.setAttribute('id', e.oldId)
  })
}

swapBackIds(res.elements)
#root {
  background-color: pink;
}

.style-from-class {
  background-color: red;
  width: 200px;
  height: 200px;
}

.style-from-id {
  background-color: green;
  width: 100px;
  height: 100px;
}
<div id="root">
  <span>normal</span>
  <span style="background: blue">inline</span>
  <div class="style-from-class">
    style-class
  </div>
  <div class="style-from-id">
    style-id
    <div style="font-size: 10px">a very nested</div>
    <div style="font-size: 12px; color: white">and another</div>
  </div>
</div>

<div id="ignored-sibling">
</div>
Preview
  • 35,317
  • 10
  • 92
  • 112
  • @Mojimi any concern with this method? – Preview Dec 14 '18 at 09:11
  • @AshishRanjan Don't do these kinds of edits, they are unecessary. – Preview Jan 30 '21 at 15:57
  • 1
    they aren't unnecessary IMO, it was to improve the quality of code. #1 - inconsistent use of semicolons, #2 - `var` -> `const`, `let`. Your answer, your rules :) – Ashish Ranjan Jan 31 '21 at 16:23
  • The 5 semis are only here for the part of the code that was taken from elsewhere, you could have removed those instead but yes I'm surprised I wasn't using const at the time already – Preview Jan 31 '21 at 20:08
  • @Preview not sure, but the snippet is returning empty ID styles? – El Mac Sep 12 '22 at 11:56
2

 
let para = document.querySelector('p');
let compStyles = window.getComputedStyle(para);
para.textContent = 'My computed font-size is ' + compStyles.getPropertyValue('font-size') + ',\nand my computed background is ' +  compStyles.getPropertyValue('background') + '.';
p {
  width: 400px;
  margin: 0 auto;
  padding: 20px;
  font: 2rem/2 sans-serif;
  text-align: center;
  background: purple;
  color: white;
}
<p>Hello</p>

you can use getComputedStyle method to get computed value of style property

Navin Gelot
  • 1,264
  • 3
  • 13
  • 32