Skip to content
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

Feat: add support for @import CSS-rule #86

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
72 changes: 72 additions & 0 deletions src/css.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,56 @@ module.exports = function( options, callback )
} );
};

var replaceImport = function( callback )
{
var args = this;
var re = new RegExp( inline.escapeSpecialChars( args.marker ), "g" );

if( inline.isBase64Path( args.src ) )
{
result = result.replace( re, () => args.rule );
return callback( null ); // Skip
}

inline.getTextReplacement( args.src, settings, function( err, content )
{
if( err )
{
result = result.replace( re, () => args.rule );
return inline.handleReplaceErr( err, args.src, settings.strict, callback );
}

var onTransform = function( err, content )
{
if( err )
{
return callback( err );
}

var rule = inline.parseCSSImportRule( args.rule );
var css = content.toString();
if( rule?.media )
{
css = "@media " + rule.media + " {\n" + css + "}\n";
}
if( rule?.supports )
{
css = "@supports (" + rule.supports + ") {\n" + css + "}\n";
}
if( rule?.layer )
{
css = "@layer " + ( typeof( rule.layer ) === "string" ? rule.layer : "" ) + " {\n" + css + "}\n";
}
result = result.replace( re, () => css );

return callback( null );
};

// recursively process imported CSS
module.exports( Object.assign( {}, settings, { fileContent: content.toString() } ), onTransform );
} );
};

var rebase = function( src )
{
var css = "url(\"" + ( inline.isRemotePath( src ) || inline.isRemotePath( settings.rebaseRelativeTo ) ? url.resolve( settings.rebaseRelativeTo, src ) : path.join( settings.rebaseRelativeTo, src ).replace( /\\/g, "/" ) ) + "\")";
Expand Down Expand Up @@ -74,6 +124,28 @@ module.exports = function( options, callback )
var inlineAttributeCommentRegex = new RegExp( "\\/\\*\\s?" + settings.inlineAttribute + "\\s?\\*\\/", "i" );
var inlineAttributeIgnoreCommentRegex = new RegExp( "\\/\\*\\s?" + settings.inlineAttribute + "-ignore\\s?\\*\\/", "i" );

index = 0;
while( ( found = inline.CSSImportRegex.exec( result.substring( index ) ) ) !== null )
{
if( !inlineAttributeIgnoreCommentRegex.test( found[ 0 ] ) &&
( settings.imports || inlineAttributeCommentRegex.test( found[ 0 ] ) ) )
{
var injectMarker = "/* @@inline-" + Math.floor( 10000 + Math.random() * 10000 ) + "@@ */";
var re = new RegExp( inline.escapeSpecialChars( found[ 0 ] ), "g" );
result = result.replace( re, injectMarker ); // replace rule immediately to prevent inlining by parallel tasks
tasks.push( replaceImport.bind(
{
rule: found[ 0 ],
src: found.groups.url || found.groups.url2,
marker: injectMarker
} ) );
}
else
{
index = found.index + index + 1;
}
}

index = 0;
while( ( found = urlRegex.exec( result.substring( index ) ) ) !== null )
{
Expand Down
58 changes: 58 additions & 0 deletions src/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,48 @@ module.exports = function( options, callback )
} );
};

var replaceStylesheet = function( callback )
{
var args = this;

var onTransform = function( err, content )
{
if( err )
{
return callback( err );
}

if( !content || typeof( args.limit ) === "number" && content.length > args.limit * 1000 )
{
return callback( null );
}

var cssOptions = Object.assign( {}, settings, {
fileContent: content.toString()
} );

css( cssOptions, function( err, content )
{
if( err )
{
return callback( err );
}
var html = content.toString();
html = html.replace( /<\/script>/gmi, "<\\/script>" );
html = "<style" + ( args.attrs ? " " + args.attrs : "" ) + ">" + html.replace( /\/\*[\s]*--[\s]*>*/gm, "/* - ->" ) + "</style>";
var re = new RegExp( inline.escapeSpecialChars( args.element ), "g" );
result = result.replace( re, () => html );
return callback( null );
} );
};

if( options.styleTransform )
{
return options.styleTransform( args.src, onTransform );
}
onTransform( null, args.src );
};

var replaceImg = function( callback )
{
var args = this;
Expand Down Expand Up @@ -222,6 +264,22 @@ module.exports = function( options, callback )
}
}

var styleRegex = /<style\b[\s\S]+?(?:\btype\s*=\s*("|')text\/css\1)?[\s\S]*?>([\s\S]*?)<\/style>/gm;
while( ( found = styleRegex.exec( result ) ) !== null )
{
if( !inlineAttributeIgnoreRegex.test( found[ 0 ] ) &&
( settings.imports || inlineAttributeRegex.test( found[ 0 ] ) ) )
{
tasks.push( replaceStylesheet.bind(
{
element: found[ 0 ],
src: found[ 2 ],
attrs: inline.getAttrs( found[ 0 ], settings ),
limit: settings.imports
} ) );
}
}

var imgRegex = /<img\b[\s\S]+?\bsrc\s*=\s*("|')([\s\S]*?)\1[\s\S]*?>/gm;
while( ( found = imgRegex.exec( result ) ) !== null )
{
Expand Down
93 changes: 93 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ util.defaults = {
svgs: 8,
scripts: true,
links: true,
imports: false,
strict: false,
relativeTo: "",
rebaseRelativeTo: "",
Expand All @@ -27,6 +28,10 @@ util.defaults = {
};

util.attrValueExpression = "(=[\"']([^\"']+?)[\"'])?";
util.CSSImportRegex = new RegExp(
'@import\\s+(?<urlExpr>(["\'])(?<url>.+?)(?<!\\\\)\\2|(?:src|url)\\(\\s*(["\'])(?<url2>.+?)(?<!\\\\)\\4\\s*\\))(?<rest>[^;]*);.*$',
"im"
);

/**
* Escape special regex characters of a particular string
Expand Down Expand Up @@ -74,6 +79,10 @@ util.getAttrs = function( tagMarkup, settings )
{
return attrs.replace( /(href|rel)=["'][^"']*["']/g, "" ).trim();
}
else if( tag === "<style" )
{
return attrs.trim();
}
}
};

Expand Down Expand Up @@ -216,3 +225,87 @@ util.handleReplaceErr = function( err, src, strict, callback )
return callback( null );
}
};

function substrInsideBalancedParentheses( str )
{
var stack = [];
var result = "";
for( var i = 0; i < str.length; i++ )
{
switch( str[ i ] )
{
case "(":
stack.push( "(" );
if( stack.length > 1 )
{
result += str[ i ];
}
break;
case ")":
if( stack[ stack.length - 1 ] === "(" )
{
stack.pop();
if( !stack.length )
{
return result;
}
result += str[ i ];
}
else
{
//not balanced!
return false;
}
break;
default:
if( stack.length )
{
result += str[ i ];
}
}
}
return result;
}

util.parseCSSImportRule = function( rule )
{
var matches = util.CSSImportRegex.exec( rule );

if( !matches || ( !matches.groups.url && !matches.groups.url2 ) )
{
return null;
}

var rest = ( matches.groups.rest || "" ).trim();
var layer = "";
var found = /^(?<layerExpr>layer(?:\s*\((?<layerName>[^)]*)\))?)/ims.exec( rest );
if( found )
{
layer = ( found.groups.layerName || "" ).trim() || true;
rest = rest.substring( found.index + found[ 0 ].length ).trim();
}

var supports = "";
if( /^supports\(/i.test( rest ) )
{
// supports(...) can't be extracted using RegEx due to nested parentheses
var expr = substrInsideBalancedParentheses( rest );
if( expr === false )
{
rest = ""; // ignore rest of the malformed rule
}
else
{
supports = expr.trim();
rest = rest.replace( expr, "" ).replace( /supports\([^)]*\)/i, "" );
}
}
var media = rest.replace( /\s*;.*$/m, "" ).trim();

return {
url: matches.groups.url || matches.groups.url2,
layer: layer || null,
supports: supports || null,
media: media || null
};
};
15 changes: 15 additions & 0 deletions test/cases/css-import-advanced.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* media queries */
@import url("css.css") print and (max-width: 800px);
/* layers */
@import url("css.css") layer;
@import url("css.css") layer(utilities);
/* feature detection */
@import url("css.css") supports(display: grid);
@import url("css.css") supports((not (display: grid)) and (display: flex));
/* all together */
@import url("css.css")
layer(base)
supports(display: grid)
screen and (max-width: 800px);

/* more styles */
70 changes: 70 additions & 0 deletions test/cases/css-import-advanced_out.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* media queries */
@media print and (max-width: 800px) {
body {
background: url(assets/other.png);
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");/*data-inline*/
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,SKIP"); /* data-inline */
}
}

/* layers */
@layer {
body {
background: url(assets/other.png);
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");/*data-inline*/
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,SKIP"); /* data-inline */
}
}

@layer utilities {
body {
background: url(assets/other.png);
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");/*data-inline*/
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,SKIP"); /* data-inline */
}
}

/* feature detection */
@supports (display: grid) {
body {
background: url(assets/other.png);
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");/*data-inline*/
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,SKIP"); /* data-inline */
}
}

@supports ((not (display: grid)) and (display: flex)) {
body {
background: url(assets/other.png);
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");/*data-inline*/
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,SKIP"); /* data-inline */
}
}

/* all together */
@layer base {
@supports (display: grid) {
@media screen and (max-width: 800px) {
body {
background: url(assets/other.png);
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");/*data-inline*/
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */
background: url("data:image/png;base64,SKIP"); /* data-inline */
}
}
}
}


/* more styles */
3 changes: 3 additions & 0 deletions test/cases/css-import.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import url("css-remote.css");

/* more styles */
14 changes: 14 additions & 0 deletions test/cases/css-import.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<style type="text/css">
@import url("css-remote.css");

/* more styles */

</style>
</head>
<body>
</body>
</html>
6 changes: 6 additions & 0 deletions test/cases/css-import_out.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
body {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");
}


/* more styles */
Loading