UNPKG

cz-typography

Version:

Universal Czech typography fixer for JavaScript, React, Next.js, and any SSR framework. Non-breaking spaces after one-letter prepositions, units, dates, ordinals and more.

450 lines (432 loc) 12.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var jsxRuntime = require('react/jsx-runtime'); var server = require('react-dom/server'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var React__default = /*#__PURE__*/_interopDefault(React); // src/TypoWrapper.js // src/rules/dates.js var FULL_DATE_REGEX = /(\d{1,2})\.[ \t]+(\d{1,2})\.[ \t]+(\d{2,4})\b/g; var DAY_MONTH_REGEX = /(\d{1,2})\.[ \t]+(\d{1,2})\.(?=[^\p{L}\p{N}]|$)/gu; function applyDates(text) { return text.replace(FULL_DATE_REGEX, "$1.\xA0$2.\xA0$3").replace(DAY_MONTH_REGEX, "$1.\xA0$2."); } // src/rules/initials.js var INITIAL_REGEX = /(\b\p{Lu})\.[ \t]+(?=\p{Lu})/gu; function applyInitials(text) { let previous; let current = text; do { previous = current; current = current.replace(INITIAL_REGEX, "$1.\xA0"); } while (current !== previous); return current; } // src/rules/ordinals.js var ORDINAL_REGEX = /(\d{1,2})\.[ \t]+(?=\p{Ll})/gu; function applyOrdinals(text) { return text.replace(ORDINAL_REGEX, "$1.\xA0"); } // src/data/prepositions.js var PREPOSITIONS = Object.freeze([ "a", "i", "k", "o", "s", "u", "v", "z" ]); // src/rules/prepositions.js var PREPOSITION_REGEX = new RegExp( `(?<=^|[\\s(\\["'\xA0\u2018\u201C\xBB\u2014\u2013-])(${PREPOSITIONS.join("|")})[ \\t]+(?=\\S)`, "giu" ); function applyPrepositions(text) { return text.replace(PREPOSITION_REGEX, "$1\xA0"); } // src/rules/roman.js var ROMAN_NUMERAL = "(?:(?=[IVXLCDM])M{0,3}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3}))"; var ROMAN_REGEX = new RegExp( `(\\b\\p{Lu}\\p{L}+)[ \\t]+(?=${ROMAN_NUMERAL}\\.(?=[^\\p{L}\\p{N}]|$))`, "gu" ); function applyRoman(text) { return text.replace(ROMAN_REGEX, "$1\xA0"); } // src/rules/thousands.js var THOUSANDS_REGEX = /(\d)[ \t](\d{3})(?=\D|$)/g; function applyThousands(text) { let previous; let current = text; do { previous = current; current = current.replace(THOUSANDS_REGEX, "$1\xA0$2"); } while (current !== previous); return current; } // src/data/units.js var SAFE_UNITS = Object.freeze( [ "km/h", "m/s", "kWh", "mAh", "mbar", "kbps", "Mbps", "Gbit", "kPa", "bar", "kg", "mg", "cm", "mm", "\xB5m", "nm", "km", "ml", "cl", "dl", "kJ", "Nm", "Wb", "lm", "lx", "cd", "mol", "min", "ms", "ns", "Hz", "Pa", "kW", "MW", "Ohm", "\xB0C", "\xB0F", "\xB0", "\u2030", "%", "px", "pt", "dpi", "bpm", "EUR", "USD", "CZK", "K\u010D", "mg/m\xB3", "str", "osob", "ks", "MJ", "TB", "GB", "MB", "kB", "bit", "GBit", "mol", "\u03A9", "\u20AC", "$", "\xA3" ].sort((a, b) => b.length - a.length) ); var STRICT_UNITS = Object.freeze( ["m", "s", "l", "t", "g", "V", "A", "W", "J", "K", "B"].sort( (a, b) => b.length - a.length ) ); // src/rules/units.js function escapeRegex(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } var SAFE_PATTERN = SAFE_UNITS.map(escapeRegex).join("|"); var STRICT_PATTERN = STRICT_UNITS.map(escapeRegex).join("|"); var SAFE_UNIT_REGEX = new RegExp( `(\\d)[ \\t]+(?=(?:${SAFE_PATTERN})(?![\\p{L}\\p{N}]))`, "gu" ); var STRICT_UNIT_REGEX = new RegExp( `(\\d)[ \\t]+(?=(?:${STRICT_PATTERN})(?:[.,;:!?)\\]/\\u00A0]|$))`, "gu" ); function applyUnits(text) { return text.replace(SAFE_UNIT_REGEX, "$1\xA0").replace(STRICT_UNIT_REGEX, "$1\xA0"); } // src/fixCzech.js var DEFAULT_OPTIONS = Object.freeze({ prepositions: true, units: true, initials: true, dates: true, ordinals: true, roman: true, thousands: true }); function fixCzech(text, options) { if (typeof text !== "string" || text.length === 0) { return text; } const opts = options ? { ...DEFAULT_OPTIONS, ...options } : DEFAULT_OPTIONS; let result = text; if (opts.thousands) result = applyThousands(result); if (opts.dates) result = applyDates(result); if (opts.ordinals) result = applyOrdinals(result); if (opts.units) result = applyUnits(result); if (opts.initials) result = applyInitials(result); if (opts.roman) result = applyRoman(result); if (opts.prepositions) result = applyPrepositions(result); return result; } // src/fixCzechDom.js var SKIPPED_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "PRE", "CODE", "TEXTAREA"]); var ELEMENT_NODE = 1; var TEXT_NODE = 3; var processedNodes = /* @__PURE__ */ new WeakSet(); function shouldSkipParent(element) { let parent = element; while (parent && parent.nodeType === ELEMENT_NODE) { if (SKIPPED_TAGS.has(parent.nodeName)) return true; if (parent.isContentEditable) return true; parent = parent.parentNode; } return false; } function processTextNode(node, options) { if (!node.nodeValue) return; if (processedNodes.has(node)) return; if (shouldSkipParent(node.parentNode)) return; const fixed = fixCzech(node.nodeValue, options); if (fixed !== node.nodeValue) { node.nodeValue = fixed; } processedNodes.add(node); } function walk(node, options) { if (!node) return; if (node.nodeType === TEXT_NODE) { processTextNode(node, options); return; } if (node.nodeType !== ELEMENT_NODE) return; if (SKIPPED_TAGS.has(node.nodeName)) return; if (node.isContentEditable) return; let child = node.firstChild; while (child) { const next = child.nextSibling; walk(child, options); child = next; } } function fixCzechDom(root, options) { if (!root || typeof root !== "object" || typeof root.nodeType !== "number") return; walk(root, options); } function observeCzechDom(root, options) { if (!root) return () => { }; fixCzechDom(root, options); if (typeof MutationObserver === "undefined") return () => { }; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === "characterData") { processedNodes.delete(mutation.target); if (mutation.target.nodeType === TEXT_NODE) { processTextNode(mutation.target, options); } continue; } for (const added of mutation.addedNodes) { walk(added, options); } } }); observer.observe(root, { childList: true, subtree: true, characterData: true }); return () => observer.disconnect(); } function processChildren(node, options) { if (typeof node === "string") { return fixCzech(node, options); } if (typeof node === "number" || typeof node === "boolean" || node == null) { return node; } if (Array.isArray(node)) { return node.map((child, index) => { const processed = processChildren(child, options); if (React__default.default.isValidElement(processed) && processed.key == null) { return React__default.default.cloneElement(processed, { key: index }); } return processed; }); } if (React__default.default.isValidElement(node)) { const props = node.props; if (props == null || props.children == null) return node; return React__default.default.cloneElement(node, { ...props, children: processChildren(props.children, options) }); } return node; } function normaliseOptions(props) { if (props.options) return props.options; const { prepositions, units, initials, dates, ordinals, roman, thousands } = props; const result = {}; if (prepositions !== void 0) result.prepositions = prepositions; if (units !== void 0) result.units = units; if (initials !== void 0) result.initials = initials; if (dates !== void 0) result.dates = dates; if (ordinals !== void 0) result.ordinals = ordinals; if (roman !== void 0) result.roman = roman; if (thousands !== void 0) result.thousands = thousands; return result; } function TypoWrapper(props) { const { children, mode = "jsx", as: Tag = "span" } = props; const options = React.useMemo( () => normaliseOptions(props), // Each tracked prop is a primitive boolean (or an object reference // explicitly supplied by the caller), so listing them keeps the // memo stable across renders that pass identical config. // eslint-disable-next-line react-hooks/exhaustive-deps [ props.options, props.prepositions, props.units, props.initials, props.dates, props.ordinals, props.roman, props.thousands ] ); const containerRef = React.useRef(null); const processedJsx = React.useMemo( () => mode === "jsx" ? processChildren(children, options) : null, [mode, children, options] ); React.useEffect(() => { if (mode !== "dom") return void 0; const target = containerRef.current; if (!target) return void 0; return observeCzechDom(target, options); }, [mode, options]); if (mode === "dom") { return /* @__PURE__ */ jsxRuntime.jsx(Tag, { ref: containerRef, style: { display: "contents" }, children }); } return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: processedJsx }); } var TypoWrapper_default = TypoWrapper; // src/fixCzechHtml.js var RAW_TEXT_TAGS = /* @__PURE__ */ new Set(["script", "style", "pre", "code", "textarea"]); var TOKEN_REGEX = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<![A-Za-z][^>]*>|<\?[\s\S]*?\?>|<\/?([A-Za-z][A-Za-z0-9-]*)\b[^>]*>|[^<]+/g; var SENTINEL = ""; function isTextToken(match) { return match.charCodeAt(0) !== 60; } function isTagToken(match) { return match.charCodeAt(0) === 60 && !match.startsWith("<!--") && !match.startsWith("<![") && !match.startsWith("<!") && !match.startsWith("<?"); } function fixWithLookahead(text, options, nextChar) { if (nextChar && /\S/.test(nextChar)) { const padded = text + nextChar + SENTINEL; const fixed = fixCzech(padded, options); if (fixed.endsWith(nextChar + SENTINEL)) { return fixed.slice(0, -(nextChar.length + SENTINEL.length)); } return fixed.endsWith(SENTINEL) ? fixed.slice(0, -SENTINEL.length) : fixed; } return fixCzech(text, options); } function transformOne(match, tagName, rawTextStack, options, nextChar) { if (!isTagToken(match) && match.charCodeAt(0) === 60) { return match; } if (isTagToken(match)) { const isClosing = match.charCodeAt(1) === 47; const tag = tagName ? tagName.toLowerCase() : ""; if (isClosing) { const top = rawTextStack[rawTextStack.length - 1]; if (top && top === tag) rawTextStack.pop(); } else if (RAW_TEXT_TAGS.has(tag) && !/\/\s*>$/.test(match)) { rawTextStack.push(tag); } return match; } if (rawTextStack.length > 0) return match; return fixWithLookahead(match, options, nextChar); } function tokenize(html) { const tokens = []; TOKEN_REGEX.lastIndex = 0; let m; while (m = TOKEN_REGEX.exec(html)) { tokens.push({ match: m[0], tagName: m[1], end: m.index + m[0].length }); } return tokens; } function lookaheadOf(tokens, index, fallback) { const next = tokens[index + 1]; if (!next) return ""; if (isTextToken(next.match)) return next.match.charAt(0); return "<"; } function transformTokens(tokens, rawTextStack, options, trailingChar) { let output = ""; for (let i = 0; i < tokens.length; i++) { const { match, tagName } = tokens[i]; const nextChar = lookaheadOf(tokens, i); output += transformOne(match, tagName, rawTextStack, options, nextChar); } return output; } function fixCzechHtml(html, options) { if (typeof html !== "string" || html.length === 0) return html; return transformTokens(tokenize(html), [], options); } function Typo({ children, options, as: Tag = "span" }) { const html = server.renderToStaticMarkup(/* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children })); const fixed = fixCzechHtml(html, options); return /* @__PURE__ */ jsxRuntime.jsx(Tag, { dangerouslySetInnerHTML: { __html: fixed } }); } function useFixCzech(text, options) { const optionsKey = options ? JSON.stringify(options) : ""; return React.useMemo( () => fixCzech(text, options), // optionsKey is a serialised representation of `options`, so it // captures the relevant dependencies without re-firing on a fresh // object literal with the same values. // eslint-disable-next-line react-hooks/exhaustive-deps [text, optionsKey] ); } exports.Typo = Typo; exports.TypoWrapper = TypoWrapper; exports.default = TypoWrapper_default; exports.fixCzech = fixCzech; exports.fixCzechDom = fixCzechDom; exports.fixCzechHtml = fixCzechHtml; exports.observeCzechDom = observeCzechDom; exports.useFixCzech = useFixCzech; //# sourceMappingURL=react.cjs.map //# sourceMappingURL=react.cjs.map