Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/Exports.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,71 @@ Default: `{ prefixes: ['--'] }`

Set this option to modify how variables are identified in a value. By default, this option is set to recognize CSS variables. For languages such as LESS and SCSS which have their own variable prefixes, additional prefixes can be added to the `prefixes` array.

##### `context`
Type: [`postcss.Input`][]<br>
Default: `undefined`

[`postcss.Input`]: http://api.postcss.org/Input.html

Set this option along with [`lineInContext`][] and [`columnInContext`][] to indicate the value's location in a larger CSS file that's been parsed by PostCSS. When these options are set, the values' `source` properties will refer to their locations inside the original CSS file which produces better error messages.

[`lineInContext`]: #lineincontext
[`columnInContext`]: #columnincontext

If this is set, `lineInContext` and `columnInContext` must be set as well. The [`parseDeclValue()`][] and [`parseAtRuleParams()`][] functions automatically set these options appropriately.

[`parseDeclValue()`]: #parsedeclvaluedecl-options
[`parseAtRuleParams()`]: #parseatruleparamsrule-options

##### `lineInContext`
Type: `Number`<br>
Default: `undefined`

Indicates the line number in the [`context`][] on which the value being parsed begins.

[`context`]: #context

If this is set, `context` and [`columnInContext`][] must be set as well. The [`parseDeclValue()`][] and [`parseAtRuleParams()`][] functions automatically set these options appropriately.

##### `columnInContext`
Type: `Number`<br>
Default: `undefined`

Indicates the column number in the [`context`][] on which the value being parsed begins.

If this is set, `context` and [`lineInContext`][] must be set as well. The [`parseDeclValue()`][] and [`parseAtRuleParams()`][] functions automatically set these options appropriately.

### `parseDeclValue(decl, options)`

A shorthand for calling [`parse()`][] on the value of a [`postcss.Declaration`][] object. This automatically sets the [`context`][], [`lineInContext`][], and [`columnInContext`][] options appropriately.

[`postcss.Declaration`]: http://api.postcss.org/Declaration.html
[`parse()`]: #parsecss-options

#### Parameters

#### `decl`
Type: [`postcss.Declaration`][]<br>
_Required_

#### `options`
Type: `Object`

### `parseAtRuleParams(rule, options)`

A shorthand for calling [`parse()`][] on the parameters of a [`postcss.AtRule`][] object. This automatically sets the [`context`][], [`lineInContext`][], and [`columnInContext`][] options appropriately.

[`postcss.AtRule`]: http://api.postcss.org/AtRule.html

#### Parameters

#### `decl`
Type: [`postcss.AtRule`][]<br>
_Required_

#### `options`
Type: `Object`

### `stringify(node, builder)`

A `Function` with a signature matching `(bit) => {}` used to concatenate or manipulate each portion (or bit) of the Node's own AST. The `nodeToString` method makes use of this, as a simple example.
Expand Down
56 changes: 56 additions & 0 deletions lib/SubInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright © 2018 Andrew Powell

This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of this Source Code Form.
*/

// A PostCSS Input that exposes a substring of a larger Input as though it were
// the entire text to be parsed.
module.exports = class SubInput {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without seeing how this is used (via tests) I can't comment on whether or not this is a good name for this.

Also, why does this not inherit from Input?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without seeing how this is used (via tests) I can't comment on whether or not this is a good name for this.

It's not used directly, just instantiated from lib/input.js.

Also, why does this not inherit from Input?

We're not re-using any of Input's methods, we're just matching its API. In static-typing terms, we're implementing Input's interface rather than extending its implementation.

constructor(css, context, lineInContext, columnInContext) {
this.css = css;
this.context = context;
this.lineInContext = lineInContext;
this.columnInContext = columnInContext;
}

error(message, line, column, opts = {}) {
let lineInContext;
let columnInContext;
if (line === 1) {
lineInContext = this.lineInContext; // eslint-disable-line prefer-destructuring
columnInContext = column + this.columnInContext - 1;
} else {
lineInContext = this.lineInContext + line - 1;
columnInContext = column;
}

return this.context.error(message, lineInContext, columnInContext, opts);
}

origin(line, column) {
let lineInContext;
let columnInContext;
if (line === 1) {
lineInContext = this.lineInContext; // eslint-disable-line prefer-destructuring
columnInContext = column + this.columnInContext - 1;
} else {
lineInContext = this.lineInContext + line - 1;
}

return this.context.origin(lineInContext, columnInContext);
}

mapResolve(file) {
return this.context.mapResolve(file);
}

get from() {
return this.context.from;
}
};
18 changes: 17 additions & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,28 @@ export interface Word extends NodeBase {

export function parse(css: string, options?: ParseOptions): Root;

export interface ParseOptions {
export function parseDeclValue(
decl: postcss.Declaration,
options?: ParseOptionsWithoutContext
): Root;

export function parseAtRuleParams(
rule: postcss.AtRule,
options?: ParseOptions
): Root;

export interface ParseOptionsWithoutContext {
ignoreUnknownWords?: boolean;
interpolation?: boolean | InterpolationOptions;
variables?: VariablesOptions;
}

export interface ParseOptions extends ParseOptionsWithoutContext {
context: postcss.Input;
lineInContext: Number;
columnInContext: Number;
}

export interface InterpolationOptions {
prefix: string;
}
Expand Down
71 changes: 69 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,58 @@
const Input = require('postcss/lib/input');

const Parser = require('./ValuesParser');
const SubInput = require('./SubInput');
const { stringify } = require('./ValuesStringifier');

const NEWLINE = '\n'.charCodeAt(0);
const FEED = '\f'.charCodeAt(0);
const CR = '\r'.charCodeAt(0);

function positionAfter(node, chunks) {
let { line } = node.source.start;
let { column } = node.source.start;
for (const chunk of chunks) {
for (let i = 0; i < chunk.length; i++) {
const code = chunk.charCodeAt(i);
if (
code === NEWLINE ||
code === FEED ||
(code === CR && chunk.charCodeAt(i + 1) !== NEWLINE)
) {
column = 1;
line += 1;
} else {
column += 1;
}
}
}

return { line, column };
}

module.exports = {
parse(css, options) {
const input = new Input(css, options);
parse(css, opts) {
const options = opts || {};

let input;
if (options.context) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options.context isn't a documented option (neither are options.lineInContext nor columnInContext) so I can't comment on them. documentation isn't only useful to end users after merge, it's a key component of new feature PR reviews.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added documentation, let me know what you think.

if (!options.lineInContext || !options.columnInContext) {
throw new RangeError(
'If the context option is passed, lineInContext and ' +
'columnInContext must also be passed.'
);
}

input = new SubInput(css, options.context, options.lineInContext, options.columnInContext);
} else if (options.lineInContext || options.columnInContext) {
throw new RangeError(
"If the context option isn't passed, lineInContext and " +
'columnInContext may not be passed.'
);
} else {
input = new Input(css, options);
}

const parser = new Parser(input, options);

parser.parse();
Expand All @@ -32,6 +79,26 @@ module.exports = {
return parser.root;
},

parseDeclValue(decl, options) {
const { line, column } = positionAfter(decl, [decl.prop, decl.raws.between]);
return module.exports.parse(decl.value, {
...options,
context: decl.source.input,
lineInContext: line,
columnInContext: column
});
},

parseAtRuleParams(rule, options) {
const { line, column } = positionAfter(rule, ['@', rule.name, rule.raws.afterName]);
return module.exports.parse(rule.params, {
...options,
context: rule.source.input,
lineInContext: line,
columnInContext: column
});
},

stringify,

nodeToString(node) {
Expand Down
7 changes: 6 additions & 1 deletion lib/nodes/Func.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ class Func extends Container {
// use a new parser to parse the params of the function. recursion here makes for easier maint
// we must require this here due to circular dependency resolution
const { parse } = require('../'); // eslint-disable-line global-require
const root = parse(params, opts);
const root = parse(params, {
...opts,
context: parser.input,
lineInContext: brackets[2],
columnInContext: brackets[3] + 1
});
const { nodes: children } = root;

// TODO: correct line and character position (should we just pad the input? probably easiest)
Expand Down
7 changes: 6 additions & 1 deletion lib/nodes/Interpolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ class Interpolation extends Container {
// use a new parser to parse the params of the function. recursion here makes for easier maint
// we must require this here due to circular dependency resolution
const { parse } = require('../'); // eslint-disable-line global-require
const { nodes: children } = parse(params, parser.options);
const { nodes: children } = parse(params, {
...parser.options,
context: parser.input,
lineInContext: first[2],
columnInContext: first[3] + first[1].length + 1
});

// TODO: correct line and character position (should we just pad the input? probably easiest)
for (const child of children) {
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"lint-staged": "^10.0.8",
"nyc": "^15.0.0",
"perfy": "^1.1.5",
"postcss-scss": "^2.0.0",
"postcss-value-parser": "^4.0.0",
"postcss-values-parser": "^3.0.3",
"pre-commit": "^1.2.2",
Expand Down
79 changes: 79 additions & 0 deletions test/errors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
Copyright © 2020 Andrew Powell

This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of this Source Code Form.
*/
const test = require('ava');

const postcss = require('postcss');
const scss = require('postcss-scss');

const { parse, parseDeclValue, parseAtRuleParams } = require('../lib');

const { throws, functionCall } = require('./fixtures/errors');

function snapshotError(t, callback) {
try {
callback();
throw Error('Expected an error.');
} catch (error) {
if (!('showSourceCode' in error)) {
throw error;
}

t.snapshot(error);
t.snapshot(error.showSourceCode(false));
}
}

test(throws.decl, (t) => {
const root = postcss.parse(throws.decl, {
from: 'file:///fixtures/errors.js'
});
snapshotError(t, () => parseDeclValue(root.nodes[0].nodes[0]));
});

test(throws.atRule, (t) => {
const root = postcss.parse(throws.atRule, {
from: 'file:///fixtures/errors.js'
});
snapshotError(t, () => parseAtRuleParams(root.nodes[0]));
});

test(throws.interpolation, (t) => {
const root = scss.parse(throws.interpolation, {
from: 'file:///fixtures/errors.js'
});

const [decl] = root.nodes[0].nodes;
snapshotError(t, () =>
parse(decl.prop, {
interpolation: { prefix: '#' },
context: root.source.input,
lineInContext: decl.source.start.line,
columnInContext: decl.source.start.column
})
);
});

test(functionCall, (t) => {
const root = scss.parse(functionCall, {
from: 'file:///fixtures/errors.js'
});

const value = parseDeclValue(root.nodes[0].nodes[0]);
value.walk((node) => {
delete node.parent; // eslint-disable-line no-param-reassign
});

snapshotError(t, () =>
value.walkFuncs((func) => {
if (func.name === 'var') throw func.error('Undefined variable!');
})
);
});
18 changes: 18 additions & 0 deletions test/fixtures/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Copyright © 2020 Andrew Powell

This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of this Source Code Form.
*/
module.exports = {
throws: {
decl: 'a {\n b: +-2.;\n}',
atRule: '@foo +-2. {\n a {\n b: c;\n }\n}',
interpolation: 'a {\n background-#{+-2.}: white;\n}'
},
functionCall: 'p {\n color: rgb(var(--some-color) / 70%);\n}'
};
Loading