UNPKG

react-style-tag

Version:

Write scoped, autoprefixed styles declaratively in React

645 lines (524 loc) 17.1 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('react-dom'), require('stylis')) : typeof define === 'function' && define.amd ? define(['exports', 'react', 'react-dom', 'stylis'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactStyleTag = {}, global.React, global.ReactDOM, global.stylis)); })(this, (function (exports, react, reactDom, stylis) { 'use strict'; 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 = stylis.compile(style); var enhancer = options.isPrefixed ? stylis.middleware([stylis.prefixer, stylis.stringify]) : stylis.stringify; return stylis.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 = react.useRef(children); var styleRef = react.useRef(); if (!styleRef.current || childrenRef.current !== children) { styleRef.current = getRenderedStyles(children, options); childrenRef.current = children; } return styleRef.current; } var Link = /*#__PURE__*/react.forwardRef(function LinkTag(_ref, ref) { var passedProps = _ref.passedProps, style = _ref.style; var getCachedLinkHref = react.useMemo(createGetCachedLinkHref, []); return /*#__PURE__*/react.createElement('link', Object.assign({}, passedProps, { href: getCachedLinkHref(style), rel: 'stylesheet', ref: ref })); }); var Style = /*#__PURE__*/react.forwardRef(function Style(props, ref) { var hasSourceMap = props.hasSourceMap, isMinified = props.isMinified, isPrefixed = props.isPrefixed; var passedProps = /*#__NOINLINE__*/ useTagProps(props); var options = react.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__*/reactDom.createPortal( /*#__PURE__*/react.createElement(Link, { passedProps: passedProps, ref: ref, style: style }), document.head); } return /*#__PURE__*/reactDom.createPortal( /*#__PURE__*/react.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; }, {}); } exports.Style = Style; exports.getGlobalOptions = getGlobalOptions; exports.hashKeys = hashKeys; exports.setGlobalOptions = setGlobalOptions; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=react-style-tag.js.map