diff --git a/WebExample/__tests__/styles.spec.ts b/WebExample/__tests__/styles.spec.ts index 562cf1fc..41ea0a00 100644 --- a/WebExample/__tests__/styles.spec.ts +++ b/WebExample/__tests__/styles.spec.ts @@ -32,11 +32,20 @@ test.describe('markdown content styling', () => { }); test('inline code', async ({page}) => { - await testMarkdownContentStyle({testContent: 'inline code', style: 'font-family: monospace; font-size: 20px; color: black; background-color: lightgray;', page}); + await testMarkdownContentStyle({ + testContent: 'inline code', + style: + 'font-family: monospace; font-size: 20px; color: black; background-color: lightgray; border-color: gray; border-width: 1px; border-radius: 4px; border-style: solid; padding: 0px; line-height: 1.5;', + page, + }); }); test('codeblock', async ({page}) => { - await testMarkdownContentStyle({testContent: 'codeblock', style: 'font-family: monospace; font-size: 20px; color: black; background-color: lightgray;', page}); + await testMarkdownContentStyle({ + testContent: 'codeblock', + style: 'border-color: gray; border-radius: 4px; padding: 0px; font-family: monospace; font-size: 20px; color: black; background-color: transparent;', + page, + }); }); test('mention-here', async ({page}) => { diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index a93833a8..3d454843 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -11,7 +11,7 @@ import type { TextInputContentSizeChangeEventData, GestureResponderEvent, } from 'react-native'; -import React, {useEffect, useRef, useCallback, useMemo, useLayoutEffect} from 'react'; +import React, {useEffect, useRef, useCallback, useMemo, useLayoutEffect, useState} from 'react'; import type {CSSProperties, MutableRefObject, ReactEventHandler, FocusEventHandler, MouseEvent, KeyboardEvent, SyntheticEvent, ClipboardEventHandler, TouchEvent} from 'react'; import {StyleSheet} from 'react-native'; import {updateInputStructure} from './web/utils/parserUtils'; @@ -21,9 +21,11 @@ import {getCurrentCursorPosition, removeSelection, setCursorPosition} from './we import './web/MarkdownTextInput.css'; import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent'; import {getElementHeight, getPlaceholderValue, isEventComposing, normalizeValue, parseInnerHTMLToText} from './web/utils/inputUtils'; -import {idGenerator, parseToReactDOMStyle, processMarkdownStyle} from './web/utils/webStyleUtils'; +import {parseToReactDOMStyle, configureCustomWebStylesheet, idGenerator, processMarkdownStyle} from './web/utils/webStyleUtils'; import {forceRefreshAllImages} from './web/inputElements/inlineImage'; +import type {PartialMarkdownStyle} from './styleUtils'; import type {MarkdownRange, InlineImagesInputProps} from './commonTypes'; +import {handleCustomStyles} from './web/inputElements/codeblock'; const useClientEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect; @@ -70,6 +72,7 @@ type MarkdownTextInputElement = HTMLDivElement & HTMLInputElement & { tree: TreeNode; uniqueId: string; + styleSheet: CSSStyleSheet; selection: Selection; imageElements: HTMLImageElement[]; }; @@ -132,7 +135,7 @@ const MarkdownTextInput = React.forwardRef(null); const currentlyFocusedField = useRef(null); const contentSelection = useRef(null); - const className = `react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`; + const [className, setClassName] = useState(`react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`); const history = useRef(null); const dimensions = useRef(null); const pasteContent = useRef(null); @@ -379,6 +382,14 @@ const MarkdownTextInput = React.forwardRef 0) { + const preBlock = preBlocks.pop() as HTMLElement; + preBlock.setAttribute('data-content', parseInnerHTMLToText(preBlock, 0)); + } + handleCustomStyles(divRef.current, processedMarkdownStyle); + } return; } let newInputUpdate: ParseTextResult; @@ -729,7 +740,7 @@ const MarkdownTextInput = React.forwardRef { + useClientEffect(() => { if (!divRef.current) { return; } @@ -737,7 +748,14 @@ const MarkdownTextInput = React.forwardRef { + if (!divRef.current) { + return; + } + handleCustomStyles(divRef.current, processedMarkdownStyle); + }; + + const resizeObserver = new ResizeObserver(handleStyles); + if (divRef.current) { + resizeObserver.observe(divRef.current); + } window.addEventListener('online', handleReconnect); return () => { window.removeEventListener('online', handleReconnect); + resizeObserver.disconnect(); }; }, [processedMarkdownStyle]); diff --git a/src/MarkdownTextInputDecoratorViewNativeComponent.ts b/src/MarkdownTextInputDecoratorViewNativeComponent.ts index 73d46cb5..b59ff826 100644 --- a/src/MarkdownTextInputDecoratorViewNativeComponent.ts +++ b/src/MarkdownTextInputDecoratorViewNativeComponent.ts @@ -25,14 +25,27 @@ interface MarkdownStyle { code: { fontFamily: string; fontSize: Float; + h1NestedFontSize?: Float; color: ColorValue; backgroundColor: ColorValue; + borderColor: ColorValue; + borderWidth: Float; + borderRadius: Float; + borderStyle: string; + padding?: Float; + paddingVertical?: Float; + paddingHorizontal?: Float; }; pre: { fontFamily: string; fontSize: Float; color: ColorValue; backgroundColor: ColorValue; + borderColor: string; + borderRadius: Float; + padding: Float; + paddingVertical?: Float; + paddingHorizontal?: Float; }; mentionHere: { color: ColorValue; diff --git a/src/__tests__/parseExpensiMark.test.ts b/src/__tests__/parseExpensiMark.test.ts index 33621d96..d52fab4d 100644 --- a/src/__tests__/parseExpensiMark.test.ts +++ b/src/__tests__/parseExpensiMark.test.ts @@ -216,8 +216,8 @@ test('inline code', () => { test('codeblock', () => { expect('```\nHello world!\n```').toBeParsedAs([ - {type: 'syntax', start: 0, length: 3}, - {type: 'pre', start: 3, length: 14}, + {type: 'syntax', start: 0, length: 4}, + {type: 'pre', start: 4, length: 13}, {type: 'syntax', start: 17, length: 3}, ]); }); diff --git a/src/__tests__/webParser.test.tsx b/src/__tests__/webParser.test.tsx index 8deeff92..53fe7c08 100644 --- a/src/__tests__/webParser.test.tsx +++ b/src/__tests__/webParser.test.tsx @@ -3,6 +3,21 @@ import {expect} from '@jest/globals'; import {parseRangesToHTMLNodes} from '../web/utils/parserUtils'; import parseExpensiMark from '../parseExpensiMark'; +jest.mock('react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle', () => ({ + __esModule: true, + default: jest.fn((style) => style), +})); + +jest.mock('react-native-web/dist/exports/StyleSheet/preprocess', () => ({ + __esModule: true, + default: jest.fn((style) => style), +})); + +jest.mock('react-native-web/dist/modules/setValueForStyles/dangerousStyleValue', () => ({ + __esModule: true, + default: jest.fn(() => null), +})); + declare module 'expect' { interface Matchers { toBeParsedAsHTML(expectedHTML: string): R; @@ -175,7 +190,7 @@ test('inline code', () => { test('codeblock', () => { expect('```\nHello world!\n```').toBeParsedAsHTML( - '

```
Hello world!
```

', + '

```
Hello world!
```

', ); }); diff --git a/src/parseExpensiMark.ts b/src/parseExpensiMark.ts index 37b951b3..80f589fe 100644 --- a/src/parseExpensiMark.ts +++ b/src/parseExpensiMark.ts @@ -174,9 +174,10 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] { } else if (node.tag === '
') { text += '\n'; } else if (node.tag.startsWith(' *[data-type='emoji'], +.react-native-live-markdown-input-multiline *[data-type='code'] > *[data-type='emoji'] { + font-size: inherit !important; + line-height: inherit !important; + vertical-align: unset !important; +} + +.react-native-live-markdown-input-multiline *[data-type='line'] *[data-type='pre'] + *[data-type='syntax'] + *[data-type='text'] { + max-width: 100%; +} + +.react-native-live-markdown-input-multiline *[data-type='line'] *[data-type='syntax']:has(+ *[data-type='pre']) { + display: grid; + line-height: 1.3; +} + +.react-native-live-markdown-input-multiline *[data-type='line'] *[data-type='syntax']:has(+ *[data-type='pre']) > *:last-child { + display: none; +} + +.react-native-live-markdown-input-multiline *[data-type='line'] *[data-type='pre'] + *[data-type='syntax'] { + display: grid; + line-height: 1.3; +} + @keyframes react-native-live-markdown-spin { 0% { transform: rotate(0deg); diff --git a/src/web/inputElements/codeblock.ts b/src/web/inputElements/codeblock.ts new file mode 100644 index 00000000..172cf690 --- /dev/null +++ b/src/web/inputElements/codeblock.ts @@ -0,0 +1,119 @@ +import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web'; +import {parseStringWithUnitToNumber} from '../../styleUtils'; +import type {PartialMarkdownStyle} from '../../styleUtils'; +import {getPropertyValue} from '../utils/webStyleUtils'; + +type Rule = {selector: string; properties: Record}; + +/** + * Applies CSS rules for code block styling, including custom styles specified in the markdownStyle object. + * Required by `pre` and `code` markdown elements, since it handles styling, resizing, and positioning of syntax characters. + */ +function handleCustomStyles(target: MarkdownTextInputElement, markdownStyle: PartialMarkdownStyle) { + if (!target.styleSheet) { + return; + } + const rules = generateCodeBlocksRules(target, markdownStyle); + addStylesheetRules(rules, target.styleSheet); +} + +function addStylesheetRules(rules: Rule[], sheet: CSSStyleSheet) { + let newSheet = ''; + rules.forEach((rule) => { + const {selector, properties} = rule; + let propertiesStr = ''; + + Object.keys(properties).forEach((prop) => { + const value = properties[prop]; + propertiesStr += `${prop}: ${value};\n`; + }); + + newSheet += `${selector}{${propertiesStr}} `; + }); + sheet.replaceSync(newSheet); +} + +function generateCodeBlocksRules(target: MarkdownTextInputElement, markdownStyle: PartialMarkdownStyle): Rule[] { + const line = target.querySelector('*[data-type="line"]:has(> *[data-type="pre"]) > span:first-child'); + if (!line) { + return []; + } + + const lineHeight = line.getBoundingClientRect()?.height; + const preStyles = markdownStyle.pre; + const padding = preStyles?.padding ?? 2; + const horizontalPadding = parseStringWithUnitToNumber(preStyles?.paddingHorizontal ?? padding); + const verticalPadding = parseStringWithUnitToNumber(preStyles?.paddingVertical ?? padding); + + const contentWidth = + target.offsetWidth - + getPropertyValue(target, 'border-left-width') - + getPropertyValue(target, 'border-left-width') - + getPropertyValue(target, 'padding-left') - + getPropertyValue(target, 'padding-right'); + + // General pre block styles + const rules: Rule[] = [ + { + selector: `.${target.uniqueId} *[data-type='pre']::before`, + properties: { + top: `${Math.floor(lineHeight)}px`, + padding: `${verticalPadding.toString()}px ${horizontalPadding.toString()}px`, + 'background-color': `${(preStyles?.backgroundColor as string) ?? 'lightgray'}`, + 'border-radius': `${preStyles?.borderRadius?.toString() ?? '4px'}`, + 'border-color': `${preStyles?.borderColor ?? 'grey'}`, + }, + }, + { + selector: `.${target.uniqueId} *[data-type='line'] *[data-type='syntax']:has(+ *[data-type='pre'])`, + properties: { + transform: `translate(-${horizontalPadding}px, -${verticalPadding}px)`, + }, + }, + { + selector: `.${target.uniqueId} *[data-type='line'] *[data-type='pre'] + *[data-type='syntax']`, + properties: { + transform: `translate(-${horizontalPadding}px, ${verticalPadding}px)`, + }, + }, + { + selector: `.${target.uniqueId} *[data-type='line'] *[data-type='pre'] + *[data-type='syntax'] + *[data-type='text']`, + properties: { + transform: `translate(-${horizontalPadding}px, ${verticalPadding}px)`, + }, + }, + { + selector: `.${target.uniqueId} *[data-type='line']:has(> *[data-type='pre']) > *:nth-child(n+4)`, + properties: { + display: 'inline-block', + transform: `translate(-${horizontalPadding}px, ${verticalPadding}px)`, + }, + }, + ]; + + // Generate style rules for all existing pre blocks + const preBlocks = [...target.querySelectorAll('*[data-type="pre"]')]; + for (let i = 0; i < preBlocks.length; i++) { + const preBlock = preBlocks[i] as HTMLElement; + const preBlockWidth = preBlock.getBoundingClientRect().width; + const preLineHeight = preBlock.parentElement?.getBoundingClientRect().height ?? 0; + + // Handle a case where something is written immediately after closing backticks without line-break + const textElementHeight = preBlock.nextElementSibling?.nextElementSibling?.getBoundingClientRect().height ?? 0; + + rules.push({ + // This selector targets specific pre block + selector: `.${target.uniqueId} *:nth-child(${i + 1} of [data-type='line']:has(> *[data-type='pre'])) > *[data-type='pre']::before`, + properties: { + height: `${preLineHeight - 2 * lineHeight - textElementHeight}px`, + 'min-width': `min(calc(100% + 2.5px), ${preBlockWidth + horizontalPadding * 2 + 1}px)`, + 'max-width': `min(${preBlockWidth + horizontalPadding * 2 + 2}px, ${contentWidth}px)`, + }, + }); + } + + return rules; +} + +// eslint-disable-next-line import/prefer-default-export +export {handleCustomStyles}; diff --git a/src/web/utils/blockUtils.ts b/src/web/utils/blockUtils.ts index 075ec12e..c0abc9f7 100644 --- a/src/web/utils/blockUtils.ts +++ b/src/web/utils/blockUtils.ts @@ -1,7 +1,9 @@ +import type {InlineImagesInputProps, MarkdownRange, MarkdownType} from '../../commonTypes'; import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web'; -import type {InlineImagesInputProps, MarkdownRange} from '../../commonTypes'; +import {parseStringWithUnitToNumber} from '../../styleUtils'; import type {PartialMarkdownStyle} from '../../styleUtils'; import {addInlineImagePreview} from '../inputElements/inlineImage'; +import BrowserUtils from './browserUtils'; import type {NodeType, TreeNode} from './treeUtils'; function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownStyle: PartialMarkdownStyle, isMultiline = true) { @@ -51,12 +53,9 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty }); break; case 'code': - Object.assign(node.style, markdownStyle.code); - break; case 'pre': - Object.assign(node.style, markdownStyle.pre); + addCodeBlockStyles(targetElement, type, markdownStyle, isMultiline); break; - case 'blockquote': Object.assign(node.style, { ...markdownStyle.blockquote, @@ -94,6 +93,56 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty } } +function addCodeBlockStyles(targetElement: HTMLElement, type: NodeType, markdownStyle: PartialMarkdownStyle, isMultiline = true) { + const node = targetElement; + + const defaultPrePadding = markdownStyle.pre?.padding ?? 2; + const preHorizontalPadding = parseStringWithUnitToNumber(markdownStyle.pre?.paddingHorizontal ?? defaultPrePadding).toString(); + const preVerticalPadding = parseStringWithUnitToNumber(markdownStyle.pre?.paddingVertical ?? defaultPrePadding).toString(); + + const defaultCodePadding = markdownStyle.code?.padding ?? 0; + const codeHorizontalPadding = parseStringWithUnitToNumber(markdownStyle.code?.paddingHorizontal ?? defaultCodePadding).toString(); + const codeVerticalPadding = parseStringWithUnitToNumber(markdownStyle.code?.paddingVertical ?? defaultCodePadding).toString(); + + switch (type) { + case 'code': + Object.assign(node.style, { + ...markdownStyle.code, + fontSize: markdownStyle.code?.h1NestedFontSize && isChildOfMarkdownElement(node, 'h1') ? markdownStyle.code.h1NestedFontSize : markdownStyle.code?.fontSize, + padding: `${codeVerticalPadding}px ${codeHorizontalPadding}px`, + lineHeight: 1.5, + }); + break; + case 'pre': + // In multiline style the pre block using pseudoelements, otherwise default to inline + if (isMultiline) { + Object.assign(node.style, { + ...markdownStyle.pre, + backgroundColor: 'transparent', + padding: 0, + }); + Object.assign((node.parentNode as HTMLElement).style, { + padding: `${preVerticalPadding}px ${preHorizontalPadding}px`, + 'line-height': BrowserUtils.isMobile ? 1.3 : 'inherit', + position: 'relative', + width: 'fit-content', + maxWidth: '100%', + boxSizing: 'border-box', + zIndex: 2, + }); + } else { + Object.assign(node.style, { + ...markdownStyle.code, + padding: `${codeVerticalPadding}px ${codeHorizontalPadding}px`, + lineHeight: 1.5, + }); + } + break; + default: + break; + } +} + const BLOCK_MARKDOWN_TYPES = ['inline-image']; const FULL_LINE_MARKDOWN_TYPES = ['blockquote']; @@ -125,4 +174,23 @@ function extendBlockStructure( return targetNode; } -export {addStyleToBlock, extendBlockStructure, isBlockMarkdownType, getFirstBlockMarkdownRange}; +function getTopParentTreeNode(node: TreeNode) { + let currentParentNode = node.parentNode; + while (currentParentNode && ['text', 'br', 'line', 'syntax'].includes(currentParentNode.parentNode?.type || '')) { + currentParentNode = currentParentNode?.parentNode || null; + } + return currentParentNode; +} + +function isChildOfMarkdownElement(node: HTMLElement, elementType: MarkdownType): boolean { + let currentNode = node.parentNode; + while (currentNode && (currentNode as HTMLElement)?.contentEditable !== 'true') { + if ((currentNode as HTMLElement)?.getAttribute?.('data-type') === elementType) { + return true; + } + currentNode = currentNode.parentNode; + } + return false; +} + +export {addStyleToBlock, extendBlockStructure, isBlockMarkdownType, getFirstBlockMarkdownRange, getTopParentTreeNode}; diff --git a/src/web/utils/cursorUtils.ts b/src/web/utils/cursorUtils.ts index adc0a1b9..f6e0bbc7 100644 --- a/src/web/utils/cursorUtils.ts +++ b/src/web/utils/cursorUtils.ts @@ -1,4 +1,5 @@ import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web'; +import {getTopParentTreeNode} from './blockUtils'; import {findHTMLElementInTree, getTreeNodeByIndex} from './treeUtils'; import type {TreeNode} from './treeUtils'; @@ -17,8 +18,15 @@ function setCursorPosition(target: MarkdownTextInputElement, startIndex: number, const range = document.createRange(); range.selectNodeContents(target); - const startTreeNode = getTreeNodeByIndex(target.tree, start); - const endTreeNode = end && startTreeNode && (end < startTreeNode.start || end >= startTreeNode.start + startTreeNode.length) ? getTreeNodeByIndex(target.tree, end) : startTreeNode; + let startTreeNode = getTreeNodeByIndex(target.tree, start); + let endTreeNode = end && startTreeNode && (end < startTreeNode.start || end >= startTreeNode.start + startTreeNode.length) ? getTreeNodeByIndex(target.tree, end) : startTreeNode; + + const parentLine = startTreeNode?.type === 'br' && getTopParentTreeNode(startTreeNode); + if (parentLine && parentLine?.childNodes?.some((e) => e.type === 'pre')) { + startTreeNode = getTreeNodeByIndex(target.tree, start - 1); + endTreeNode = startTreeNode; + } + if (!startTreeNode || !endTreeNode) { console.error('Invalid start or end tree node'); return; diff --git a/src/web/utils/inputUtils.ts b/src/web/utils/inputUtils.ts index 2fbf7a3f..e4e4f403 100644 --- a/src/web/utils/inputUtils.ts +++ b/src/web/utils/inputUtils.ts @@ -37,7 +37,7 @@ function normalizeValue(value: string) { } // Parses the HTML structure of a MarkdownTextInputElement to a plain text string. Used for getting the correct value of the input element. -function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition: number, inputType?: string): string { +function parseInnerHTMLToText(target: MarkdownTextInputElement | HTMLElement, cursorPosition: number, inputType?: string): string { // Returns the parent of a given node that is higher in the hierarchy and is of a different type than 'text', 'br' or 'line' function getTopParentNode(node: ChildNode) { let currentParentNode = node.parentNode; @@ -104,7 +104,7 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition: text = text.replaceAll('\r\n', '\n'); // Force letter removal if the input value haven't changed but input type is 'delete' - if (text === target.value && inputType?.includes('delete')) { + if ('value' in target && text === target?.value && inputType?.includes('delete')) { text = text.slice(0, cursorPosition - 1) + text.slice(cursorPosition); } return text; diff --git a/src/web/utils/parserUtils.ts b/src/web/utils/parserUtils.ts index 869abb9c..933ab3b5 100644 --- a/src/web/utils/parserUtils.ts +++ b/src/web/utils/parserUtils.ts @@ -7,6 +7,7 @@ import {addStyleToBlock, extendBlockStructure, getFirstBlockMarkdownRange, isBlo import type {InlineImagesInputProps, MarkdownRange} from '../../commonTypes'; import {getAnimationCurrentTimes, updateAnimationsTime} from './animationUtils'; import {sortRanges, ungroupRanges} from '../../rangeUtils'; +import {handleCustomStyles} from '../inputElements/codeblock'; type Paragraph = { text: string; @@ -212,13 +213,12 @@ function parseRangesToHTMLNodes( // create markdown span element const span = document.createElement('span') as HTMLMarkdownElement; span.setAttribute('data-type', range.type); + const spanNode = appendNode(span, currentParentNode, range.type, range.length); if (!disableInlineStyles) { addStyleToBlock(span, range.type, markdownStyle, isMultiline); } - const spanNode = appendNode(span, currentParentNode, range.type, range.length); - if (isMultiline && !disableInlineStyles && currentInput) { currentParentNode = extendBlockStructure(currentInput, currentParentNode, range, lineMarkdownRanges, text, markdownStyle, inlineImagesProps); } @@ -313,6 +313,7 @@ function updateInputStructure( targetElement.tree = createRootTreeNode(targetElement); } + handleCustomStyles(target, markdownStyle); return {text, cursorPosition: cursorPosition || 0}; } diff --git a/src/web/utils/treeUtils.ts b/src/web/utils/treeUtils.ts index c4cbd866..667f8ee1 100644 --- a/src/web/utils/treeUtils.ts +++ b/src/web/utils/treeUtils.ts @@ -1,5 +1,6 @@ import type {HTMLMarkdownElement} from '../../MarkdownTextInput.web'; import type {MarkdownRange, MarkdownType} from '../../commonTypes'; +import {parseInnerHTMLToText} from './inputUtils'; type NodeType = MarkdownType | 'line' | 'text' | 'br' | 'block' | 'root'; @@ -74,6 +75,10 @@ function updateTreeElementRefs(treeRoot: TreeNode, element: HTMLMarkdownElement) if (currentElement) { node.element = currentElement; } + + if (currentElement?.dataset.type === 'pre') { + currentElement.setAttribute('data-content', parseInnerHTMLToText(currentElement as HTMLElement, 0)); + } } return treeRoot; diff --git a/src/web/utils/webStyleUtils.ts b/src/web/utils/webStyleUtils.ts index 9a5eb609..c1b2f982 100644 --- a/src/web/utils/webStyleUtils.ts +++ b/src/web/utils/webStyleUtils.ts @@ -60,4 +60,14 @@ function* generateUniqueId() { const idGenerator = generateUniqueId(); -export {parseToReactDOMStyle, processMarkdownStyle, idGenerator}; +function configureCustomWebStylesheet(): CSSStyleSheet | null { + const sheet = new CSSStyleSheet(); + document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; + return sheet; +} + +function getPropertyValue(e: HTMLElement, p: string) { + return parseFloat(window.getComputedStyle(e).getPropertyValue(p).replace('px', '')); +} + +export {parseToReactDOMStyle, processMarkdownStyle, idGenerator, configureCustomWebStylesheet, getPropertyValue};