react-style-tag
Version:
Write scoped, autoprefixed styles declaratively in React
636 lines (517 loc) • 15.5 kB
JavaScript
import { forwardRef, useMemo, createElement, useRef } from 'react';
import { createPortal } from 'react-dom';
import { compile, middleware, prefixer, stringify, serialize } from 'stylis';
var noop = function noopCreateObjectUrl() {
return;
};
/**
* Create a cached version of the getLinkHref.
*/
function createGetCachedLinkHref() {
var href;
var createObjectURL = getCreateObjectURL();
var currentStyle = null;
return function getCachedLinkedHref(style) {
if (style === currentStyle) {
return href;
}
if (createObjectURL === noop) {
createObjectURL = getCreateObjectURL();
}
if (currentStyle = style) {
return href = createObjectURL(new Blob([style], {
type: 'text/css'
}));
}
return href = undefined;
};
}
/**
* Create the url string based on the available URL. If window is unavailable (such as in SSR),
* then bail out.
*/
function getCreateObjectURL() {
if (typeof window === 'undefined') {
return noop;
}
var URL = window.URL || window.webkitURL;
return URL.createObjectURL || noop;
}
var _process$env;
var IS_PRODUCTION = typeof process !== 'undefined' && ((_process$env = process.env) == null ? void 0 : _process$env.NODE_ENV) === 'production';
/**
* The global options to apply as fallback to local props.
*/
var DEFAULT_OPTIONS = {
hasSourceMap: !IS_PRODUCTION,
isMinified: IS_PRODUCTION,
isPrefixed: true
};
var globalOptions = Object.assign({}, DEFAULT_OPTIONS);
var hasOwnProperty = Object.prototype.hasOwnProperty;
function getGlobalOptions() {
return globalOptions;
}
function normalizeOptions(options) {
var normalized = Object.assign({}, globalOptions);
var option;
for (option in options) {
if (hasOwnProperty.call(normalized, option) && options[option] != null) {
normalized[option] = !!options[option];
}
}
return normalized;
}
/**
* Set the options passed to be global.
*/
function setGlobalOptions(options) {
var option;
for (option in options) {
if (hasOwnProperty.call(globalOptions, option)) {
globalOptions[option] = !!options[option];
}
}
}
// FIXME: handle Unicode characters
function isName(character) {
return character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || character >= '0' && character <= '9' || '-_*.:#[]'.indexOf(character) >= 0;
}
function isQuote(_char) {
return _char === "'" || _char === '"';
}
function isWhitespace(_char2) {
return _char2 === ' ' || _char2 === '\n' || _char2 === '\t' || _char2 === '\r' || _char2 === '\f';
}
function beautify(style, options) {
if (options === void 0) {
options = {};
}
// We want to deal with LF (\n) only
style = style.replace(/\r\n/g, '\n');
var _options = options,
_options$autosemicolo = _options.autosemicolon,
autosemicolon = _options$autosemicolo === void 0 ? false : _options$autosemicolo,
_options$indent = _options.indent,
indent = _options$indent === void 0 ? ' ' : _options$indent,
_options$openbrace = _options.openbrace,
openbracesuffix = _options$openbrace === void 0 ? true : _options$openbrace;
var index = 0;
var length = style.length;
var blocks = [];
var formatted = '';
var character;
var character2;
var string;
var State = {
Start: 0,
AtRule: 1,
Block: 2,
Selector: 3,
Ruleset: 4,
Property: 5,
Separator: 6,
Expression: 7,
URL: 8
};
var state = State.Start;
var depth = 0;
var quote;
var comment = false;
function appendIndent() {
for (var _index = depth; _index > 0; --_index) {
formatted += indent;
}
}
function openBlock() {
formatted = formatted.trimEnd();
if (openbracesuffix) {
formatted += ' {';
} else {
formatted += '\n';
appendIndent();
formatted += '{';
}
if (character2 !== '\n') {
formatted += '\n';
}
depth += 1;
}
function closeBlock() {
var last;
depth -= 1;
formatted = formatted.trimEnd();
if (formatted.length > 0 && autosemicolon) {
last = formatted.charAt(formatted.length - 1);
if (last !== ';' && last !== '{') {
formatted += ';';
}
}
formatted += '\n';
appendIndent();
formatted += '}';
blocks.push(formatted);
formatted = '';
}
while (index < length) {
character = style.charAt(index);
character2 = style.charAt(index + 1);
++index; // Inside a string literal?
if (isQuote(quote)) {
formatted += character;
if (character === quote) {
quote = null;
}
if (character === '\\' && character2 === quote) {
// Don't treat escaped character as the closing quote
formatted += character2;
++index;
}
continue;
} // Starting a string literal?
if (isQuote(character)) {
formatted += character;
quote = character;
continue;
} // Comment
if (comment) {
formatted += character;
if (character === '*' && character2 === '/') {
comment = false;
formatted += character2;
++index;
}
continue;
}
if (character === '/' && character2 === '*') {
comment = true;
formatted += character;
formatted += character2;
++index;
continue;
}
if (state === State.Start) {
if (blocks.length === 0 && isWhitespace(character) && formatted.length === 0) {
continue;
} // Copy white spaces and control characters
if (character <= ' ' || character.charCodeAt(0) >= 128) {
state = State.Start;
formatted += character;
continue;
} // Selector or at-rule
if (isName(character) || character === '@') {
// Clear trailing whitespaces and linefeeds.
string = formatted.trimEnd();
if (string.length === 0) {
// If we have empty string after removing all the trailing
// spaces, that means we are right after a block.
// Ensure a blank line as the separator.
if (blocks.length > 0) {
formatted = '\n\n';
}
} else {
var lastChar = string.charAt(string.length - 1); // After finishing a ruleset or directive statement,
// there should be one blank line.
if (lastChar === '}' || lastChar === ';') {
formatted = string + '\n\n';
} else {
// After block comment, keep all the linefeeds but
// start from the first column (remove whitespaces prefix).
// eslint-disable-next-line no-constant-condition
while (true) {
character2 = formatted.charAt(formatted.length - 1);
if (character2 !== ' ' && character2.charCodeAt(0) !== 9) {
break;
}
formatted = formatted.substr(0, formatted.length - 1);
}
}
}
formatted += character;
state = character === '@' ? State.AtRule : State.Selector;
continue;
}
}
if (state === State.AtRule) {
// ';' terminates a statement.
if (character === ';') {
formatted += character;
state = State.Start;
continue;
} // '{' starts a block
if (character === '{') {
string = formatted.trimEnd();
openBlock();
state = string === '@font-face' ? State.Ruleset : State.Block;
continue;
}
formatted += character;
continue;
}
if (state === State.Block) {
// Selector
if (isName(character)) {
// Clear trailing whitespaces and linefeeds.
string = formatted.trimEnd();
if (string.length === 0) {
// If we have empty string after removing all the trailing
// spaces, that means we are right after a block.
// Ensure a blank line as the separator.
if (blocks.length > 0) {
formatted = '\n\n';
}
} else {
// Insert blank line if necessary.
if (string.charAt(string.length - 1) === '}') {
formatted = string + '\n\n';
} else {
// After block comment, keep all the linefeeds but
// start from the first column (remove whitespaces prefix).
// eslint-disable-next-line no-constant-condition
while (true) {
character2 = formatted.charAt(formatted.length - 1);
if (character2 !== ' ' && character2.charCodeAt(0) !== 9) {
break;
}
formatted = formatted.substr(0, formatted.length - 1);
}
}
}
appendIndent();
formatted += character;
state = State.Selector;
continue;
} // '}' resets the state.
if (character === '}') {
closeBlock();
state = State.Start;
continue;
}
formatted += character;
continue;
}
if (state === State.Selector) {
// '{' starts the ruleset.
if (character === '{') {
openBlock();
state = State.Ruleset;
continue;
} // '}' resets the state.
if (character === '}') {
closeBlock();
state = State.Start;
continue;
}
formatted += character;
continue;
}
if (state === State.Ruleset) {
// '}' finishes the ruleset.
if (character === '}') {
closeBlock();
state = State.Start;
if (depth > 0) {
state = State.Block;
}
continue;
} // Make sure there is no blank line or trailing spaces inbetween
if (character === '\n') {
formatted = formatted.trimEnd();
formatted += '\n';
continue;
} // property name
if (!isWhitespace(character)) {
formatted = formatted.trimEnd();
formatted += '\n';
appendIndent();
formatted += character;
state = State.Property;
continue;
}
formatted += character;
continue;
}
if (state === State.Property) {
// ':' concludes the property.
if (character === ':') {
formatted = formatted.trimEnd();
formatted += ': ';
state = State.Expression;
if (isWhitespace(character2)) {
state = State.Separator;
}
continue;
} // '}' finishes the ruleset.
if (character === '}') {
closeBlock();
state = State.Start;
if (depth > 0) {
state = State.Block;
}
continue;
}
formatted += character;
continue;
}
if (state === State.Separator) {
// Non-whitespace starts the expression.
if (!isWhitespace(character)) {
formatted += character;
state = State.Expression;
continue;
} // Anticipate string literal.
if (isQuote(character2)) {
state = State.Expression;
}
continue;
}
if (state === State.Expression) {
// '}' finishes the ruleset.
if (character === '}') {
closeBlock();
state = State.Start;
if (depth > 0) {
state = State.Block;
}
continue;
} // ';' completes the declaration.
if (character === ';') {
formatted = formatted.trimEnd();
formatted += ';\n';
state = State.Ruleset;
continue;
}
formatted += character;
if (character === '(' && formatted.charAt(formatted.length - 2) === 'l' && formatted.charAt(formatted.length - 3) === 'r' && formatted.charAt(formatted.length - 4) === 'u') {
// URL starts with '(' and closes with ')'.
state = State.URL;
continue;
}
continue;
}
if (state === State.URL && // ')' finishes the URL (only if it is not escaped).
character === ')' && formatted.charAt( // @ts-expect-error - testing multiline
formatted.length - 1 !== '\\' ? 1 : 0)) {
formatted += character;
state = State.Expression;
continue;
} // The default action is to copy the character (to prevent
// infinite loop).
formatted += character;
}
formatted = blocks.join('') + formatted;
return formatted;
}
/**
* Get the styles processed for passing through to the element.
*/
function getProcessedStyles(style, options) {
var compiled = compile(style);
var enhancer = options.isPrefixed ? middleware([prefixer, stringify]) : stringify;
return serialize(compiled, enhancer);
}
/**
* Get the styles rendered in the HTML tag.
*/
function getRenderedStyles(style, options) {
var processed = getProcessedStyles(style, options);
return options.isMinified ? processed :
/*#__NOINLINE__*/
beautify(processed, {
autosemicolon: true,
indent: ' '
});
}
var INTERNAL_PROPS = {
children: true,
hasSourceMap: true,
isMinified: true,
isPrefixed: true
};
/**
* Extract the props used for deriving processed style for passing through to the
* underlying HTML element.
*/
function useTagProps(props) {
var remainingProps = {};
for (var key in props) {
if (!INTERNAL_PROPS[key]) {
remainingProps[key] = props[key];
}
}
return remainingProps;
}
/**
* Calculate and store the style in a local reference.
*/
function useStyle(children, options) {
var childrenRef = useRef(children);
var styleRef = useRef();
if (!styleRef.current || childrenRef.current !== children) {
styleRef.current = getRenderedStyles(children, options);
childrenRef.current = children;
}
return styleRef.current;
}
var Link = /*#__PURE__*/forwardRef(function LinkTag(_ref, ref) {
var passedProps = _ref.passedProps,
style = _ref.style;
var getCachedLinkHref = useMemo(createGetCachedLinkHref, []);
return /*#__PURE__*/createElement('link', Object.assign({}, passedProps, {
href: getCachedLinkHref(style),
rel: 'stylesheet',
ref: ref
}));
});
var Style = /*#__PURE__*/forwardRef(function Style(props, ref) {
var hasSourceMap = props.hasSourceMap,
isMinified = props.isMinified,
isPrefixed = props.isPrefixed;
var passedProps =
/*#__NOINLINE__*/
useTagProps(props);
var options = useMemo(function () {
return normalizeOptions({
hasSourceMap: hasSourceMap,
isMinified: isMinified,
isPrefixed: isPrefixed
});
}, [hasSourceMap, isMinified, isPrefixed]);
var style =
/*#__NOINLINE__*/
useStyle(props.children, options);
if (options.hasSourceMap) {
return /*#__PURE__*/createPortal( /*#__PURE__*/createElement(Link, {
passedProps: passedProps,
ref: ref,
style: style
}), document.head);
}
return /*#__PURE__*/createPortal( /*#__PURE__*/createElement('style', Object.assign({}, passedProps, {
ref: ref
}), style), document.head);
});
var counter = 0;
/**
* Simple bitwise hash of string value.
*/
function hash(key) {
var stringToHash = key + "-" + counter++;
var hashValue = 5381;
var index = stringToHash.length;
while (index) {
hashValue = hashValue * 33 ^ stringToHash.charCodeAt(--index);
}
return "scoped__" + key + "__" + (hashValue >>> 0);
}
/**
* Create a hash map based on the keys passed.
*/
function hashKeys(keys) {
return keys.reduce(function (hashMap, key) {
hashMap[key] = hash(key);
return hashMap;
}, {});
}
export { Style, getGlobalOptions, hashKeys, setGlobalOptions };
//# sourceMappingURL=react-style-tag.esm.js.map