Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F101534063
inlineStyles.js
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Tue, Feb 11, 08:34
Size
11 KB
Mime Type
text/html
Expires
Thu, Feb 13, 08:34 (1 d, 19 h)
Engine
blob
Format
Raw Data
Handle
24178460
Attached To
rOACCT Open Access Compliance Check Tool (OACCT)
inlineStyles.js
View Options
'use strict'
;
/**
* @typedef {import('../lib/types').Specificity} Specificity
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').XastParent} XastParent
*/
const
csstree
=
require
(
'css-tree'
);
// @ts-ignore not defined in @types/csso
const
specificity
=
require
(
'csso/lib/restructure/prepare/specificity'
);
const
stable
=
require
(
'stable'
);
const
{
visitSkip
,
querySelectorAll
,
detachNodeFromParent
,
}
=
require
(
'../lib/xast.js'
);
exports
.
type
=
'visitor'
;
exports
.
name
=
'inlineStyles'
;
exports
.
active
=
true
;
exports
.
description
=
'inline styles (additional options)'
;
/**
* Compares two selector specificities.
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
*
* @type {(a: Specificity, b: Specificity) => number}
*/
const
compareSpecificity
=
(
a
,
b
)
=>
{
for
(
var
i
=
0
;
i
<
4
;
i
+=
1
)
{
if
(
a
[
i
]
<
b
[
i
])
{
return
-
1
;
}
else
if
(
a
[
i
]
>
b
[
i
])
{
return
1
;
}
}
return
0
;
};
/**
* Moves + merges styles from style elements to element styles
*
* Options
* onlyMatchedOnce (default: true)
* inline only selectors that match once
*
* removeMatchedSelectors (default: true)
* clean up matched selectors,
* leave selectors that hadn't matched
*
* useMqs (default: ['', 'screen'])
* what media queries to be used
* empty string element for styles outside media queries
*
* usePseudos (default: [''])
* what pseudo-classes/-elements to be used
* empty string element for all non-pseudo-classes and/or -elements
*
* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<{
* onlyMatchedOnce?: boolean,
* removeMatchedSelectors?: boolean,
* useMqs?: Array<string>,
* usePseudos?: Array<string>
* }>}
*/
exports
.
fn
=
(
root
,
params
)
=>
{
const
{
onlyMatchedOnce
=
true
,
removeMatchedSelectors
=
true
,
useMqs
=
[
''
,
'screen'
],
usePseudos
=
[
''
],
}
=
params
;
/**
* @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>}
*/
const
styles
=
[];
/**
* @type {Array<{
* node: csstree.Selector,
* item: csstree.ListItem<csstree.CssNode>,
* rule: csstree.Rule,
* matchedElements?: Array<XastElement>
* }>}
*/
let
selectors
=
[];
return
{
element
:
{
enter
:
(
node
,
parentNode
)
=>
{
// skip <foreignObject /> content
if
(
node
.
name
===
'foreignObject'
)
{
return
visitSkip
;
}
// collect only non-empty <style /> elements
if
(
node
.
name
!==
'style'
||
node
.
children
.
length
===
0
)
{
return
;
}
// values other than the empty string or text/css are not used
if
(
node
.
attributes
.
type
!=
null
&&
node
.
attributes
.
type
!==
''
&&
node
.
attributes
.
type
!==
'text/css'
)
{
return
;
}
// parse css in style element
let
cssText
=
''
;
for
(
const
child
of
node
.
children
)
{
if
(
child
.
type
===
'text'
||
child
.
type
===
'cdata'
)
{
cssText
+=
child
.
value
;
}
}
/**
* @type {null | csstree.CssNode}
*/
let
cssAst
=
null
;
try
{
cssAst
=
csstree
.
parse
(
cssText
,
{
parseValue
:
false
,
parseCustomProperty
:
false
,
});
}
catch
{
return
;
}
if
(
cssAst
.
type
===
'StyleSheet'
)
{
styles
.
push
({
node
,
parentNode
,
cssAst
});
}
// collect selectors
csstree
.
walk
(
cssAst
,
{
visit
:
'Selector'
,
enter
(
node
,
item
)
{
const
atrule
=
this
.
atrule
;
const
rule
=
this
.
rule
;
if
(
rule
==
null
)
{
return
;
}
// skip media queries not included into useMqs param
let
mq
=
''
;
if
(
atrule
!=
null
)
{
mq
=
atrule
.
name
;
if
(
atrule
.
prelude
!=
null
)
{
mq
+=
`
$
{
csstree
.
generate
(
atrule
.
prelude
)}
`
;
}
}
if
(
useMqs
.
includes
(
mq
)
===
false
)
{
return
;
}
/**
* @type {Array<{
* item: csstree.ListItem<csstree.CssNode>,
* list: csstree.List<csstree.CssNode>
* }>}
*/
const
pseudos
=
[];
if
(
node
.
type
===
'Selector'
)
{
node
.
children
.
each
((
childNode
,
childItem
,
childList
)
=>
{
if
(
childNode
.
type
===
'PseudoClassSelector'
||
childNode
.
type
===
'PseudoElementSelector'
)
{
pseudos
.
push
({
item
:
childItem
,
list
:
childList
});
}
});
}
// skip pseudo classes and pseudo elements not includes into usePseudos param
const
pseudoSelectors
=
csstree
.
generate
({
type
:
'Selector'
,
children
:
new
csstree
.
List
().
fromArray
(
pseudos
.
map
((
pseudo
)
=>
pseudo
.
item
.
data
)
),
});
if
(
usePseudos
.
includes
(
pseudoSelectors
)
===
false
)
{
return
;
}
// remove pseudo classes and elements to allow querySelector match elements
// TODO this is not very accurate since some pseudo classes like first-child
// are used for selection
for
(
const
pseudo
of
pseudos
)
{
pseudo
.
list
.
remove
(
pseudo
.
item
);
}
selectors
.
push
({
node
,
item
,
rule
});
},
});
},
},
root
:
{
exit
:
()
=>
{
if
(
styles
.
length
===
0
)
{
return
;
}
// stable sort selectors
const
sortedSelectors
=
stable
(
selectors
,
(
a
,
b
)
=>
{
const
aSpecificity
=
specificity
(
a
.
item
.
data
);
const
bSpecificity
=
specificity
(
b
.
item
.
data
);
return
compareSpecificity
(
aSpecificity
,
bSpecificity
);
}).
reverse
();
for
(
const
selector
of
sortedSelectors
)
{
// match selectors
const
selectorText
=
csstree
.
generate
(
selector
.
item
.
data
);
/**
* @type {Array<XastElement>}
*/
const
matchedElements
=
[];
try
{
for
(
const
node
of
querySelectorAll
(
root
,
selectorText
))
{
if
(
node
.
type
===
'element'
)
{
matchedElements
.
push
(
node
);
}
}
}
catch
(
selectError
)
{
continue
;
}
// nothing selected
if
(
matchedElements
.
length
===
0
)
{
continue
;
}
// apply styles to matched elements
// skip selectors that match more than once if option onlyMatchedOnce is enabled
if
(
onlyMatchedOnce
&&
matchedElements
.
length
>
1
)
{
continue
;
}
// apply <style/> to matched elements
for
(
const
selectedEl
of
matchedElements
)
{
const
styleDeclarationList
=
csstree
.
parse
(
selectedEl
.
attributes
.
style
==
null
?
''
:
selectedEl
.
attributes
.
style
,
{
context
:
'declarationList'
,
parseValue
:
false
,
}
);
if
(
styleDeclarationList
.
type
!==
'DeclarationList'
)
{
continue
;
}
const
styleDeclarationItems
=
new
Map
();
csstree
.
walk
(
styleDeclarationList
,
{
visit
:
'Declaration'
,
enter
(
node
,
item
)
{
styleDeclarationItems
.
set
(
node
.
property
,
item
);
},
});
// merge declarations
csstree
.
walk
(
selector
.
rule
,
{
visit
:
'Declaration'
,
enter
(
ruleDeclaration
)
{
// existing inline styles have higher priority
// no inline styles, external styles, external styles used
// inline styles, external styles same priority as inline styles, inline styles used
// inline styles, external styles higher priority than inline styles, external styles used
const
matchedItem
=
styleDeclarationItems
.
get
(
ruleDeclaration
.
property
);
const
ruleDeclarationItem
=
styleDeclarationList
.
children
.
createItem
(
ruleDeclaration
);
if
(
matchedItem
==
null
)
{
styleDeclarationList
.
children
.
append
(
ruleDeclarationItem
);
}
else
if
(
matchedItem
.
data
.
important
!==
true
&&
ruleDeclaration
.
important
===
true
)
{
styleDeclarationList
.
children
.
replace
(
matchedItem
,
ruleDeclarationItem
);
styleDeclarationItems
.
set
(
ruleDeclaration
.
property
,
ruleDeclarationItem
);
}
},
});
selectedEl
.
attributes
.
style
=
csstree
.
generate
(
styleDeclarationList
);
}
if
(
removeMatchedSelectors
&&
matchedElements
.
length
!==
0
&&
selector
.
rule
.
prelude
.
type
===
'SelectorList'
)
{
// clean up matching simple selectors if option removeMatchedSelectors is enabled
selector
.
rule
.
prelude
.
children
.
remove
(
selector
.
item
);
}
selector
.
matchedElements
=
matchedElements
;
}
// no further processing required
if
(
removeMatchedSelectors
===
false
)
{
return
;
}
// clean up matched class + ID attribute values
for
(
const
selector
of
sortedSelectors
)
{
if
(
selector
.
matchedElements
==
null
)
{
continue
;
}
if
(
onlyMatchedOnce
&&
selector
.
matchedElements
.
length
>
1
)
{
// skip selectors that match more than once if option onlyMatchedOnce is enabled
continue
;
}
for
(
const
selectedEl
of
selector
.
matchedElements
)
{
// class
const
classList
=
new
Set
(
selectedEl
.
attributes
.
class
==
null
?
null
:
selectedEl
.
attributes
.
class
.
split
(
' '
)
);
const
firstSubSelector
=
selector
.
node
.
children
.
first
();
if
(
firstSubSelector
!=
null
&&
firstSubSelector
.
type
===
'ClassSelector'
)
{
classList
.
delete
(
firstSubSelector
.
name
);
}
if
(
classList
.
size
===
0
)
{
delete
selectedEl
.
attributes
.
class
;
}
else
{
selectedEl
.
attributes
.
class
=
Array
.
from
(
classList
).
join
(
' '
);
}
// ID
if
(
firstSubSelector
!=
null
&&
firstSubSelector
.
type
===
'IdSelector'
)
{
if
(
selectedEl
.
attributes
.
id
===
firstSubSelector
.
name
)
{
delete
selectedEl
.
attributes
.
id
;
}
}
}
}
for
(
const
style
of
styles
)
{
csstree
.
walk
(
style
.
cssAst
,
{
visit
:
'Rule'
,
enter
:
function
(
node
,
item
,
list
)
{
// clean up <style/> rulesets without any css selectors left
if
(
node
.
type
===
'Rule'
&&
node
.
prelude
.
type
===
'SelectorList'
&&
node
.
prelude
.
children
.
isEmpty
()
)
{
list
.
remove
(
item
);
}
},
});
if
(
style
.
cssAst
.
children
.
isEmpty
())
{
// remove emtpy style element
detachNodeFromParent
(
style
.
node
,
style
.
parentNode
);
}
else
{
// update style element if any styles left
const
firstChild
=
style
.
node
.
children
[
0
];
if
(
firstChild
.
type
===
'text'
||
firstChild
.
type
===
'cdata'
)
{
firstChild
.
value
=
csstree
.
generate
(
style
.
cssAst
);
}
}
}
},
},
};
};
Event Timeline
Log In to Comment