Skip to content

postcss-rewrite-url: url modifiers and docs #1526

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

Merged
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
5 changes: 5 additions & 0 deletions plugins/postcss-rewrite-url/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changes to PostCSS Rewrite URL

### Unreleased (minor)

- Add support for rewriting url modifiers (e.g. `rewrite-url('foo.png' --foo)` -> `rewrite-url('foo.png#foo')`)
- Document the syntax definition for `rewrite-url()`

### 2.0.4

_November 1, 2024_
Expand Down
63 changes: 57 additions & 6 deletions plugins/postcss-rewrite-url/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,42 +71,93 @@ instructions for:
Determine how urls are rewritten with the `rewriter` callback.

```ts
export interface ValueToRewrite {
url: string
interface ValueToRewrite {
url: string;
urlModifiers: Array<string>;
}

export interface RewriteContext {
interface RewriteContext {
type: 'declaration-value' | 'at-rule-prelude';
from: string | undefined;
rootFrom: string | undefined;
property?: string;
atRuleName?: string;
}

export type Rewriter = (value: ValueToRewrite, context: RewriteContext) => ValueToRewrite | false;
type Rewriter = (value: ValueToRewrite, context: RewriteContext) => ValueToRewrite | false;

/** postcss-rewrite-url plugin options */
export type pluginOptions = {
type pluginOptions = {
rewriter: Rewriter;
};
```

```js
postcssRewriteURL({
rewriter: (value, context) => {
console.log(value); // info about the `rewrite-url()` function itself (e.g. the url and url modifiers)
console.log(context); // context surrounding the `rewrite-url()` function (i.e. where was it found?)

if (value.url === 'ignore-me') {
// return `false` to ignore this url and preserve `rewrite-url()` in the output
return false;
}

console.log(context); // for extra conditional logic
// use url modifiers to trigger specific behavior
if (value.urlModifiers.includes('--a-custom-modifier')) {
return {
url: value.url + '#other-modification',
urlModifiers: [], // pass new or existing url modifiers to emit these in the final result
};
}

return {
url: value.url + '#modified',
};
},
})
```

## Syntax

[PostCSS Rewrite URL] is non-standard and is not part of any official CSS Specification.

### `rewrite-url()` function

The `rewrite-url()` function takes a url string and optional url modifiers and will be transformed to a standard `url()` function by a dev tool.

```css
.foo {
background: rewrite-url('foo.png');
}
```

```
rewrite-url() = rewrite-url( <string> <url-modifier>* )
```

#### [Stylelint](https://stylelint.io/user-guide/rules/declaration-property-value-no-unknown/#propertiessyntax--property-syntax-)

Stylelint is able to check for unknown property values.
Setting the correct configuration for this rule makes it possible to check even non-standard syntax.

```js
'declaration-property-value-no-unknown': [
true,
{
"typesSyntax": {
"url": "| rewrite-url( <string> <url-modifier>* )"
}
},
],
'function-no-unknown': [
true,
{
"ignoreFunctions": ["rewrite-url"]
}
],
```

[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test

[discord]: https://discord.gg/bUadyRwkJS
Expand Down
2 changes: 1 addition & 1 deletion plugins/postcss-rewrite-url/dist/index.cjs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"use strict";var e=require("@csstools/css-tokenizer"),r=require("@csstools/css-parser-algorithms");function serializeString(e){let r="";for(const t of e){const e=t.codePointAt(0);if(void 0!==e)switch(e){case 0:r+=String.fromCodePoint(65533);break;case 127:r+=`\\${e.toString(16)}`;break;case 34:case 39:case 92:r+=`\\${t}`;break;default:if(1<=e&&e<=31){r+=`\\${e.toString(16)} `;break}r+=t}else r+=String.fromCodePoint(65533)}return r}const t=/rewrite-url\(/i,o=/^rewrite-url$/i,creator=e=>{const r=e?.rewriter??(e=>e);return{postcssPlugin:"postcss-rewrite-url",Once(e,{result:t}){e.walkDecls((e=>{rewriteDeclaration(e,t,r)})),e.walkAtRules((e=>{rewriteAtRule(e,t,r)}))},Declaration(e,{result:t}){rewriteDeclaration(e,t,r)},AtRule(e,{result:t}){rewriteAtRule(e,t,r)}}};function rewriteDeclaration(e,r,o){if(!t.test(e.value))return;const s={type:"declaration-value",rootFrom:r.opts.from,from:e.source?.input.from,property:e.prop},i=rewrite(o,e.value,s);i!==e.value&&(e.value=i)}function rewriteAtRule(e,r,o){if(!t.test(e.params))return;const s={type:"at-rule-prelude",rootFrom:r.opts.from,from:e.source?.input.from,atRuleName:e.name},i=rewrite(o,e.params,s);i!==e.params&&(e.params=i)}function rewrite(t,s,i){const a=r.parseCommaSeparatedListOfComponentValues(e.tokenize({css:s})),n=r.replaceComponentValues(a,(s=>{if(r.isFunctionNode(s)&&o.test(s.getName()))for(const o of s.value)if(!r.isWhitespaceNode(o)&&!r.isCommentNode(o)&&r.isTokenNode(o)&&e.isTokenString(o.value)){const e=o.value[4].value.trim(),r=t({url:e},i);if(!1===r)return;if(r.url===e)break;return o.value[4].value=r.url,o.value[1]=`"${serializeString(r.url)}"`,s.name[1]="url(",s.name[4].value="url",s}}));return r.stringify(n)}creator.postcss=!0,module.exports=creator;
"use strict";var e=require("@csstools/css-tokenizer"),r=require("@csstools/css-parser-algorithms");function serializeString(e){let r="";for(const t of e){const e=t.codePointAt(0);if(void 0!==e)switch(e){case 0:r+=String.fromCodePoint(65533);break;case 127:r+=`\\${e.toString(16)}`;break;case 34:case 39:case 92:r+=`\\${t}`;break;default:if(1<=e&&e<=31){r+=`\\${e.toString(16)} `;break}r+=t}else r+=String.fromCodePoint(65533)}return r}const t=/rewrite-url\(/i,o=/^rewrite-url$/i,creator=e=>{const r=e?.rewriter??(e=>e);return{postcssPlugin:"postcss-rewrite-url",Once(e,{result:t}){e.walkDecls((e=>{rewriteDeclaration(e,t,r)})),e.walkAtRules((e=>{rewriteAtRule(e,t,r)}))},Declaration(e,{result:t}){rewriteDeclaration(e,t,r)},AtRule(e,{result:t}){rewriteAtRule(e,t,r)}}};function rewriteDeclaration(e,r,o){if(!t.test(e.value))return;const s={type:"declaration-value",rootFrom:r.opts.from,from:e.source?.input.from,property:e.prop},i=rewrite(o,e.value,s);i!==e.value&&(e.value=i)}function rewriteAtRule(e,r,o){if(!t.test(e.params))return;const s={type:"at-rule-prelude",rootFrom:r.opts.from,from:e.source?.input.from,atRuleName:e.name},i=rewrite(o,e.params,s);i!==e.params&&(e.params=i)}function rewrite(t,s,i){const n=r.parseCommaSeparatedListOfComponentValues(e.tokenize({css:s})),a=r.replaceComponentValues(n,(s=>{if(!r.isFunctionNode(s)||!o.test(s.getName()))return;const n=s.value.filter((e=>!r.isWhiteSpaceOrCommentNode(e)));for(let o=0;o<n.length;o++){const a=n[o];if(r.isTokenNode(a)&&e.isTokenString(a.value)){const u=a.value[4].value.trim(),l=n.slice(o+1),c=t({url:u,urlModifiers:l.map((e=>e.toString()))},i);if(!1===c)return;const m=r.parseListOfComponentValues(e.tokenize({css:[`"${serializeString(c.url)}"`,...c.urlModifiers??l].join(" ")}));return s.value=m,s.name[1]="url(",s.name[4].value="url",s}return}}));return r.stringify(a)}creator.postcss=!0,module.exports=creator;
1 change: 1 addition & 0 deletions plugins/postcss-rewrite-url/dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export declare type Rewriter = (value: ValueToRewrite, context: RewriteContext)

export declare interface ValueToRewrite {
url: string;
urlModifiers: Array<string>;
}

export { }
2 changes: 1 addition & 1 deletion plugins/postcss-rewrite-url/dist/index.mjs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import{tokenize as r,isTokenString as e}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as t,replaceComponentValues as o,isFunctionNode as s,isWhitespaceNode as a,isCommentNode as i,isTokenNode as l,stringify as u}from"@csstools/css-parser-algorithms";function serializeString(r){let e="";for(const t of r){const r=t.codePointAt(0);if(void 0!==r)switch(r){case 0:e+=String.fromCodePoint(65533);break;case 127:e+=`\\${r.toString(16)}`;break;case 34:case 39:case 92:e+=`\\${t}`;break;default:if(1<=r&&r<=31){e+=`\\${r.toString(16)} `;break}e+=t}else e+=String.fromCodePoint(65533)}return e}const n=/rewrite-url\(/i,c=/^rewrite-url$/i,creator=r=>{const e=r?.rewriter??(r=>r);return{postcssPlugin:"postcss-rewrite-url",Once(r,{result:t}){r.walkDecls((r=>{rewriteDeclaration(r,t,e)})),r.walkAtRules((r=>{rewriteAtRule(r,t,e)}))},Declaration(r,{result:t}){rewriteDeclaration(r,t,e)},AtRule(r,{result:t}){rewriteAtRule(r,t,e)}}};function rewriteDeclaration(r,e,t){if(!n.test(r.value))return;const o={type:"declaration-value",rootFrom:e.opts.from,from:r.source?.input.from,property:r.prop},s=rewrite(t,r.value,o);s!==r.value&&(r.value=s)}function rewriteAtRule(r,e,t){if(!n.test(r.params))return;const o={type:"at-rule-prelude",rootFrom:e.opts.from,from:r.source?.input.from,atRuleName:r.name},s=rewrite(t,r.params,o);s!==r.params&&(r.params=s)}function rewrite(n,f,m){const p=t(r({css:f})),w=o(p,(r=>{if(s(r)&&c.test(r.getName()))for(const t of r.value)if(!a(t)&&!i(t)&&l(t)&&e(t.value)){const e=t.value[4].value.trim(),o=n({url:e},m);if(!1===o)return;if(o.url===e)break;return t.value[4].value=o.url,t.value[1]=`"${serializeString(o.url)}"`,r.name[1]="url(",r.name[4].value="url",r}}));return u(w)}creator.postcss=!0;export{creator as default};
import{tokenize as r,isTokenString as e}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as t,replaceComponentValues as o,isFunctionNode as s,isWhiteSpaceOrCommentNode as i,isTokenNode as a,parseListOfComponentValues as l,stringify as n}from"@csstools/css-parser-algorithms";function serializeString(r){let e="";for(const t of r){const r=t.codePointAt(0);if(void 0!==r)switch(r){case 0:e+=String.fromCodePoint(65533);break;case 127:e+=`\\${r.toString(16)}`;break;case 34:case 39:case 92:e+=`\\${t}`;break;default:if(1<=r&&r<=31){e+=`\\${r.toString(16)} `;break}e+=t}else e+=String.fromCodePoint(65533)}return e}const u=/rewrite-url\(/i,c=/^rewrite-url$/i,creator=r=>{const e=r?.rewriter??(r=>r);return{postcssPlugin:"postcss-rewrite-url",Once(r,{result:t}){r.walkDecls((r=>{rewriteDeclaration(r,t,e)})),r.walkAtRules((r=>{rewriteAtRule(r,t,e)}))},Declaration(r,{result:t}){rewriteDeclaration(r,t,e)},AtRule(r,{result:t}){rewriteAtRule(r,t,e)}}};function rewriteDeclaration(r,e,t){if(!u.test(r.value))return;const o={type:"declaration-value",rootFrom:e.opts.from,from:r.source?.input.from,property:r.prop},s=rewrite(t,r.value,o);s!==r.value&&(r.value=s)}function rewriteAtRule(r,e,t){if(!u.test(r.params))return;const o={type:"at-rule-prelude",rootFrom:e.opts.from,from:r.source?.input.from,atRuleName:r.name},s=rewrite(t,r.params,o);s!==r.params&&(r.params=s)}function rewrite(u,f,m){const p=t(r({css:f})),w=o(p,(t=>{if(!s(t)||!c.test(t.getName()))return;const o=t.value.filter((r=>!i(r)));for(let s=0;s<o.length;s++){const i=o[s];if(a(i)&&e(i.value)){const e=i.value[4].value.trim(),a=o.slice(s+1),n=u({url:e,urlModifiers:a.map((r=>r.toString()))},m);if(!1===n)return;const c=l(r({css:[`"${serializeString(n.url)}"`,...n.urlModifiers??a].join(" ")}));return t.value=c,t.name[1]="url(",t.name[4].value="url",t}return}}));return n(w)}creator.postcss=!0;export{creator as default};
63 changes: 57 additions & 6 deletions plugins/postcss-rewrite-url/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,40 +38,91 @@
Determine how urls are rewritten with the `rewriter` callback.

```ts
export interface ValueToRewrite {
url: string
interface ValueToRewrite {
url: string;
urlModifiers: Array<string>;
}

export interface RewriteContext {
interface RewriteContext {
type: 'declaration-value' | 'at-rule-prelude';
from: string | undefined;
rootFrom: string | undefined;
property?: string;
atRuleName?: string;
}

export type Rewriter = (value: ValueToRewrite, context: RewriteContext) => ValueToRewrite | false;
type Rewriter = (value: ValueToRewrite, context: RewriteContext) => ValueToRewrite | false;

/** postcss-rewrite-url plugin options */
export type pluginOptions = {
type pluginOptions = {
rewriter: Rewriter;
};
```

```js
<exportName>({
rewriter: (value, context) => {
console.log(value); // info about the `rewrite-url()` function itself (e.g. the url and url modifiers)
console.log(context); // context surrounding the `rewrite-url()` function (i.e. where was it found?)

if (value.url === 'ignore-me') {
// return `false` to ignore this url and preserve `rewrite-url()` in the output
return false;
}

console.log(context); // for extra conditional logic
// use url modifiers to trigger specific behavior
if (value.urlModifiers.includes('--a-custom-modifier')) {
return {
url: value.url + '#other-modification',
urlModifiers: [], // pass new or existing url modifiers to emit these in the final result
};
}

return {
url: value.url + '#modified',
};
},
})
```

## Syntax

[<humanReadableName>] is non-standard and is not part of any official CSS Specification.

### `rewrite-url()` function

The `rewrite-url()` function takes a url string and optional url modifiers and will be transformed to a standard `url()` function by a dev tool.

```css
.foo {
background: rewrite-url('foo.png');
}
```

```
rewrite-url() = rewrite-url( <string> <url-modifier>* )
```

#### [Stylelint](https://stylelint.io/user-guide/rules/declaration-property-value-no-unknown/#propertiessyntax--property-syntax-)

Stylelint is able to check for unknown property values.
Setting the correct configuration for this rule makes it possible to check even non-standard syntax.

```js
'declaration-property-value-no-unknown': [
true,
{
"typesSyntax": {
"url": "| rewrite-url( <string> <url-modifier>* )"
}
},
],
'function-no-unknown': [
true,
{
"ignoreFunctions": ["rewrite-url"]
}
],
```

<linkList>
34 changes: 21 additions & 13 deletions plugins/postcss-rewrite-url/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { AtRule, Declaration, PluginCreator, Result } from 'postcss';
import { isTokenString, tokenize } from '@csstools/css-tokenizer';
import { isCommentNode, isFunctionNode, isTokenNode, isWhitespaceNode, parseCommaSeparatedListOfComponentValues, replaceComponentValues, stringify } from '@csstools/css-parser-algorithms';
import { isFunctionNode, isTokenNode, isWhiteSpaceOrCommentNode, parseCommaSeparatedListOfComponentValues, parseListOfComponentValues, replaceComponentValues, stringify } from '@csstools/css-parser-algorithms';
import { serializeString } from './serialize-string';

export interface ValueToRewrite {
url: string
url: string;
urlModifiers: Array<string>;
}

export interface RewriteContext {
Expand Down Expand Up @@ -102,30 +103,37 @@ function rewrite(rewriter: Rewriter, value: string, context: RewriteContext): st
return;
}

for (const x of componentValue.value) {
if (isWhitespaceNode(x) || isCommentNode(x)) {
continue;
}
const rewriteArguments = componentValue.value.filter((x) => !isWhiteSpaceOrCommentNode(x));

for (let i = 0; i < rewriteArguments.length; i++) {
const x = rewriteArguments[i];

if (isTokenNode(x) && isTokenString(x.value)) {
const original = x.value[4].value.trim();
const modified = rewriter({ url: original }, context);
const urlModifiers = rewriteArguments.slice(i + 1);

const modified = rewriter({ url: original, urlModifiers: urlModifiers.map((y) => y.toString()) }, context);
if (modified === false) {
return;
}

if (modified.url === original) {
break;
}

x.value[4].value = modified.url;
x.value[1] = `"${serializeString(modified.url)}"`;
const modifiedArguments = parseListOfComponentValues(
tokenize({
css: [
`"${serializeString(modified.url)}"`,
...(modified.urlModifiers ?? urlModifiers)
].join(' ')
})
);

componentValue.value = modifiedArguments;
componentValue.name[1] = 'url(';
componentValue.name[4].value = 'url';

return componentValue;
}

return;
}
},
);
Expand Down
27 changes: 27 additions & 0 deletions plugins/postcss-rewrite-url/test/_tape.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,33 @@ postcssTape(plugin)({
return false;
}

if (value.urlModifiers.includes('--url-modifier-a')) {
return {
url: value.url + '#modified-a',
};
}

if (value.urlModifiers.includes('--url-modifier-b')) {
return {
url: value.url + '#modified-b',
urlModifiers: [],
};
}

if (value.urlModifiers.includes('--url-modifier-c')) {
return {
url: value.url + '#modified-c',
urlModifiers: ['crossorigin(anonymous)'],
};
}

if (value.urlModifiers.includes('--url-modifier-d')) {
return {
url: value.url + '#modified-d',
urlModifiers: value.urlModifiers.filter((x) => x !== '--url-modifier-d'),
};
}

return {
url: value.url + '#modified',
};
Expand Down
24 changes: 24 additions & 0 deletions plugins/postcss-rewrite-url/test/basic.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,28 @@
background: url('foo.png');
}

.ignore {
background: rewrite-url(oops 'foo.png');
}

@foo rewrite-url('foo.png');

.url-modifiers {
/* No overrides, url modifiers should be preserved */
background: rewrite-url('foo.png' --url-modifier-a crossorigin(use-credentials) referrerpolicy(no-referrer));
}

.url-modifiers {
/* Empty list, all url modifiers should be removed */
background: rewrite-url('foo.png' --url-modifier-b crossorigin(use-credentials) referrerpolicy(no-referrer));
}

.url-modifiers {
/* A single explicit item, only that item should be present */
background: rewrite-url('foo.png' --url-modifier-c crossorigin(use-credentials) referrerpolicy(no-referrer));
}

.url-modifiers {
/* A filter on the original values, only a single item should be removed */
background: rewrite-url('foo.png' --url-modifier-d crossorigin(use-credentials) referrerpolicy(no-referrer));
}
24 changes: 24 additions & 0 deletions plugins/postcss-rewrite-url/test/basic.expect.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,28 @@
background: url('foo.png');
}

.ignore {
background: rewrite-url(oops 'foo.png');
}

@foo url("foo.png#modified");

.url-modifiers {
/* No overrides, url modifiers should be preserved */
background: url("foo.png#modified-a" --url-modifier-a crossorigin(use-credentials) referrerpolicy(no-referrer));
}

.url-modifiers {
/* Empty list, all url modifiers should be removed */
background: url("foo.png#modified-b");
}

.url-modifiers {
/* A single explicit item, only that item should be present */
background: url("foo.png#modified-c" crossorigin(anonymous));
}

.url-modifiers {
/* A filter on the original values, only a single item should be removed */
background: url("foo.png#modified-d" crossorigin(use-credentials) referrerpolicy(no-referrer));
}