Syntaxes like `postcss-html` adjust node source locations relative to enclosing non-css documents

See: https://github.com/ota-meshi/postcss-html/blob/168b49076ad66fe0e8766cb6ee899fa1534d3680/lib/html/parse-styles.js#L55-L76

For example:

<p>Hello</p>

<style>
p {
  color: green;
}
</style>

The p {} rule when parsed with postcss-html has source locations that are offset relative to the entire html doc.

So p {} starts on line 4, not on line 1.

This is very useful when reporting errors (i.e. Stylelint) to developers in a code editors.
As the error will be reported on the line and column in the entire document.


However this unfortunately conflicts with the changes I introduced in #1980

Since those changes, we use node.source.input.css to infer positions.
But we assume the start of node.source.input.css corresponds with:

  • index == 0
  • line == 1 and column == 1

Because postcss-html offsets the positions relative to the enclosing document this assumptions proves to be incorrect, making any position calculations incorrect.


The simplest way forward I could think of was to add an extra field on Input to keep track of both the source of the enclosing document and of the CSS block independently.

class Input {
constructor(css, opts = {}) {
if (
css === null ||
typeof css === 'undefined' ||
(typeof css === 'object' && !css.toString)
) {
throw new Error(`PostCSS received ${css} instead of CSS string`)
}
this.css = css.toString()
if (this.css[0] === '\uFEFF' || this.css[0] === '\uFFFE') {
this.hasBOM = true
this.css = this.css.slice(1)
} else {
this.hasBOM = false
}
class Input {
  constructor(css, opts = {}) {
    if (
      css === null ||
      typeof css === 'undefined' ||
      (typeof css === 'object' && !css.toString)
    ) {
      throw new Error(`PostCSS received ${css} instead of CSS string`)
    }

    this.css = css.toString()

    if (this.css[0] === '\uFEFF' || this.css[0] === '\uFFFE') {
      this.hasBOM = true
      this.css = this.css.slice(1)
    } else {
      this.hasBOM = false
    }

    this.document = this.css
    if (opts.document) this.document = opts.document.toString()

    // ...

For almost all usage of PostCSS input.document would correspond to input.css as the document is the CSS stylesheet.

But for CSS-in-X syntaxes (like postcss-html) the syntax authors could set document to the enclosing source (e.g. the html document).


When inferring positions we would use node.source.input.document instead of node.source.input.css in PostCSS itself.

  positionBy(opts) {
    let pos = this.source.start
    if (opts.index) {
      pos = this.positionInside(opts.index)
    } else if (opts.word) {
      let stringRepresentation = this.source.input.document.slice(
        sourceOffset(this.source.input.document, this.source.start),
        sourceOffset(this.source.input.document, this.source.end)
      )
      let index = stringRepresentation.indexOf(opts.word)
      if (index !== -1) pos = this.positionInside(index)
    }
    return pos
  }