4

During investigation of advantages and disadvantages of attaching CSS with <?xml-stylesheet> processing instruction, I came upon some issues.

Suppose we have a simple XHTML document (which is delivered with application/xhtml+xml MIME type and viewed in a Web browser):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>A sample XHTML document</title>
    <script type="application/javascript" src="/script.js"></script>
  </head>
  <body>
    <h1>A heading</h1>
  </body>
</html>

Then we have an external CSS file (let it be named style.css and put in root directory):

h1 { color: red; }

At first, in script.js, I dynamically attach this CSS with a link element:

const link = document.createElement('link');
Object.entries({rel: 'stylesheet', type: 'text/css', href: '/style.css'})
      .forEach(([name, value]) => link.setAttribute(name, value));
document.head.appendChild(link);

Then the script is waiting until the stylesheet finishes loading and reaches it through sheet property:

link.addEventListener('load', function() {
  const stylesheet = link.sheet;
});

After this, the script can manipulate this stylesheet, for example:

stylesheet.cssRules.item(0).style.color = 'green';      // modify an existing rule
stylesheet.insertRule('body { background: #ffc; }', 1); // insert a new rule

But now, I cannot figure out whether the same manipulations are possible if a stylesheet is attached with <?xml-stylesheet> processing instruction:

const pi = document.createProcessingInstruction('xml-stylesheet',
           'href="/style.css" type="text/css"');
document.insertBefore(pi, document.documentElement);

First, PI seem not to have load event, so the script cannot know when the stylesheet is ready. Second, there is nothing like sheet property, so you cannot call pi.sheet to reach the stylesheet.

Is there any way to overcome these difficulties and to get from the script to the stylesheet associated with <?xml-stylesheet> PI?

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
  • What are you trying to achieve using processing instruction? – guest271314 Jan 06 '17 at 01:03
  • @guest271314, I am investigating advantages and disadvantages of attaching stylesheets with ``. –  Jan 06 '17 at 01:10
  • _"There aren’t any events for this object, and it doesn’t have any properties for getting its stylesheet."_ Not certain what Question is? Are you trying to get and parse a `StyleSheet` loaded within an `xhtml` `document`? Can you include `xhtml` `document` and what you have tried, and describe requirement at Question? – guest271314 Jan 06 '17 at 01:13
  • I don't think you can "do" anything with processor instructions at all, actually. If you need that level of control over loading of your stylesheet, use a . On the other hand, if all you need is to know when all stylesheets have finished loading, you can use `window.onload`... – Mr Lister Jan 06 '17 at 07:34
  • @MrLister, my script inserts `<!xml-stylesheet>` dinamically, long after `window.onload` has fired. –  Jan 06 '17 at 13:36
  • @Displayname Oh... I didn't even know you could insert processing instructions dynamically. My gut feeling now says, just use an element already. – Mr Lister Jan 06 '17 at 13:40
  • @guest271314, I do something like this: `const pi = document.createProcessingInstruction('xml-stylesheet', 'href="style.css" type="text/css"'); document.insertBefore(pi, document.documentElement);` Then I want to know when the stylesheet finishes loading and to access this stylesheet from JS. But at the time, I found no way for any of these tasks. –  Jan 06 '17 at 13:45
  • @Displayname Can you include full `xhtml` and `javascript` at Question to demonstrate what you have tried? What is `document` at `document.createProcessingInstruction()`? Is `document.documentElement` a `ProcessingInstruction` node, or the root node of the `html` portion of `document`? What do you mean by "process this stylesheet from JS"? What needs to be processed? What exactly do you need to process in the `stylesheet`? Why do you not include the processing instruction within the `xhtml` portion of `document`? – guest271314 Jan 06 '17 at 16:54
  • @Displayname Can you include full `xhtm`l and `javascript` at Question to demonstrate what you have tried? What is `document` at `document.createProcessingInstruction()`? Is `document.documentElement` a `ProcessingInstruction` node, or the root node of the `html` portion of `document`? What do you mean by "process this stylesheet from JS"? What exactly do you need to process in the stylesheet? – guest271314 Jan 06 '17 at 17:04
  • @Displayname See http://stackoverflow.com/help/mcve – guest271314 Jan 06 '17 at 17:15
  • @guest271314, I have revised and expanded the question, so there is nothing more to prevent you from giving your competent and helpful answer. –  Jan 06 '17 at 18:38

1 Answers1

0

First, PI seem not to have load event, so the script cannot know when the stylesheet is ready.

You can use PerformanceObserver to check for requested and loaded resources. Iterates nodes of document, check for .nodeType 7 or .nodeType 8, as ProcessingInstruction node could have comment .nodeType. Get "resource" property from performance entries. Parse .nodeValue of filtered node for URL at href="URL", check if value is equal to "resource" of performance entry, then check if .styleSheet .href value is equal to parsed URL, and if parsed URL is equal to performance entry "resource" property value. If true, iterate .cssRules or .rules of the styleSheet loaded at ProcessingInstruction node.

window.onload = () => {
  let resource;
  const observer = new PerformanceObserver((list, obj) => {
    for (let entry of list.getEntries()) {
      for (let [key, prop] of Object.entries(entry.toJSON())) {
        if (key === "name") {
          resource = prop;
          var nodes = document.childNodes;
          _nodes: for (let node of nodes) {
            if (node.nodeType === 7 || node.nodeType === 8 
            && node.nodeValue === pi.nodeValue) {
              let url = node.baseURI 
                        + node.nodeValue.match(/[^href="][a-z0-9/.]+/i)[0];
              if (url === resource) {
                observer.disconnect();
                // use `setTimeout` here for
                // low RAM, busy CPU, many processes running
                let stylesheets = node.rootNode.styleSheets;
                for (let xmlstyle of stylesheets) {
                  if (xmlstyle.href === url && url === resource) {
                    let rules = (xmlstyle["cssRules"] || xmlstyle["rules"]);
                    for (let rule of rules) {
                      // do stuff
                      console.log(rule, rule.cssText, rule.style, xmlstyle);
                      break _nodes;
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  });

  observer.observe({
    entryTypes: ["resource"]
  });

  const pi = document.createProcessingInstruction('xml-stylesheet',
    'href="style.css" type="text/css"');
  document.insertBefore(pi, document.documentElement);

}

plnkr http://plnkr.co/edit/uXfSzu0dMDCOfZbsdA7n?p=preview

You can also use MutationObserver, setTimeout() to handle

low RAM, busy CPU, many processes running

window.onload = function() {
  let observer = new MutationObserver(function(mutations) {
    console.log(mutations)
    for (let mutation of mutations) {
      for (let node of mutation.addedNodes) {
        if (node.nodeName === "xml-stylesheet") {
          let url = node.baseURI 
                    + node.nodeValue.match(/[^href="][a-z0-9/.]+/i)[0];
          setTimeout(function() {
            for (let style of document.styleSheets) {
              if (style.href === url) {
                observer.disconnect();
                // do stuff
                console.log(style)
              }
            }
          // adjust `duration` to compensate for device
          // low RAM, busy CPU, many processes running
          }, 500)  
        }
      }
    }
  });

  observer.observe(document, {
    childList: true
  });

  const pi = document.createProcessingInstruction('xml-stylesheet',
    'href="style.css" type="text/css"');
  document.insertBefore(pi, document.documentElement);

}

plnkr http://plnkr.co/edit/AI4QZiBUx6f1Kmc5qNG9?p=preview


Alternatively, use XMLHttpRequest() or fetch() to request .css file, create and append <style> element to document, do stuff with response text, set .textContent of style element to adjusted css text.

guest271314
  • 1
  • 15
  • 104
  • 177
  • Unfortunately, `PerformanceObserver` can only tell when the resource (e.g. `style.css`) is loaded, i.e. when all its bytes are received by browser engine. But it cannot tell when the new stylesheet is really added to the list of `document.styleSheets` and its properties like `cssRules` are available for scripting. –  Jan 07 '17 at 10:59
  • On powerful devices, there is no problem. The observer “catches” a loaded CSS file, and it is added to the list so fast that the near-term call of `document.styleSheets` returns the already updated list with a new stylesheet. But testing the code on a device with low capabilities (low RAM, busy CPU, many processes running), I discovered that the stylesheet is not in time for the call of `document.styleSheets`. This call returns the old stylesheets list and cannot find the requested stylesheet in it. Only after a second or two `document.styleSheets` is updated. –  Jan 07 '17 at 10:59
  • I think the real solution may be to observe `document.styleSheets` directly and to catch events when a new stylesheet is added to it. But I don’t know how to do it (except for using `setInterval`). –  Jan 07 '17 at 10:59
  • @HydrochoerusHydrochaeris _"But testing the code on a device with low capabilities (low RAM, busy CPU, many processes running), I discovered that the stylesheet is not in time for the call of document.styleSheets"_ You can use `setTimeout()` at either `PerformanceObserver` or `MutationObserver` approach. Adjust `duration` of `setTimeout` to compensate for the specific device. – guest271314 Jan 07 '17 at 17:29