@tiptap/core
Version:
headless rich text editor
110 lines (91 loc) • 3.06 kB
text/typescript
/** Splits a CSS style string into declarations, ignoring semicolons inside quotes/parentheses. */
function splitStyleDeclarations(styles: string): string[] {
const result: string[] = []
let current = ''
let inSingleQuote = false
let inDoubleQuote = false
let parenDepth = 0
const length = styles.length
for (let i = 0; i < length; i += 1) {
const char = styles[i]
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote
current += char
continue
}
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote
current += char
continue
}
if (!inSingleQuote && !inDoubleQuote) {
if (char === '(') {
parenDepth += 1
current += char
continue
}
if (char === ')' && parenDepth > 0) {
parenDepth -= 1
current += char
continue
}
if (char === ';' && parenDepth === 0) {
result.push(current)
current = ''
continue
}
}
current += char
}
if (current) {
result.push(current)
}
return result
}
/** Yields property/value pairs from a style string. */
function parseStyleEntries(styles: string | undefined): [property: string, value: string][] {
const pairs: [string, string][] = []
const declarations = splitStyleDeclarations(styles || '')
const numDeclarations = declarations.length
for (let i = 0; i < numDeclarations; i += 1) {
const declaration = declarations[i]
const firstColonIndex = declaration.indexOf(':')
if (firstColonIndex === -1) {
continue
}
const property = declaration.slice(0, firstColonIndex).trim()
const value = declaration.slice(firstColonIndex + 1).trim()
if (property && value) {
pairs.push([property, value])
}
}
return pairs
}
export function mergeAttributes(...objects: Record<string, any>[]): Record<string, any> {
return objects
.filter(item => !!item)
.reduce((items, item) => {
const mergedAttributes = { ...items }
Object.entries(item).forEach(([key, value]) => {
const exists = mergedAttributes[key]
if (!exists) {
mergedAttributes[key] = value
return
}
if (key === 'class') {
const valueClasses: string[] = value ? String(value).split(' ') : []
const existingClasses: string[] = mergedAttributes[key] ? mergedAttributes[key].split(' ') : []
const insertClasses = valueClasses.filter(valueClass => !existingClasses.includes(valueClass))
mergedAttributes[key] = [...existingClasses, ...insertClasses].join(' ')
} else if (key === 'style') {
const styleMap = new Map([...parseStyleEntries(mergedAttributes[key]), ...parseStyleEntries(value)])
mergedAttributes[key] = Array.from(styleMap.entries())
.map(([property, val]) => `${property}: ${val}`)
.join('; ')
} else {
mergedAttributes[key] = value
}
})
return mergedAttributes
}, {})
}