Skip to content

Commit

Permalink
feat: improve markup support (#302)
Browse files Browse the repository at this point in the history
* feat: add support for basic markup

* fix: don't take fast path on entities
  • Loading branch information
brownie-in-motion authored Jan 26, 2024
1 parent 71e3432 commit 56c5686
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 68 deletions.
68 changes: 0 additions & 68 deletions src/components/Player/ClueText.js

This file was deleted.

75 changes: 75 additions & 0 deletions src/components/Player/ClueText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { createElement, Fragment, ReactNode } from 'react'

type Tree =
| { name?: string; children: Tree[] }
| { name: 'text'; value: string }

// parse HTML by creating a template element and walking its tree
// keep only elements and their contents (i.e. no attributes)
const simpleParse = (clue: string): Tree => {
const template = document.createElement('template')
template.innerHTML = clue

const tree: Tree = { children: [] }
const stack: [Tree, Node][] = [[tree, template.content]]

while (stack.length) {
const [parent, node] = stack.pop()!

// we never push text nodes onto the stack, so this should not happen
if (!('children' in parent)) throw new Error('tree invariant broken')
if (!node.hasChildNodes()) continue

for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i]
if (child.nodeType === Node.TEXT_NODE) {
parent.children.push({
name: 'text',
value: child.nodeValue!,
})
} else if (child.nodeType === Node.ELEMENT_NODE) {
const { tagName } = child as Element
const treeChild: Tree = {
name: tagName.toLowerCase(),
children: [],
}
parent.children.push(treeChild)
stack.push([treeChild, child])
}
}
}

template.remove()

return tree
}

// render allowed elements into React elements
const simpleRender = (tree: Tree, allowed: string[]): ReactNode => {
if (tree.name === 'text') return 'value' in tree ? tree.value : ''

// if the name is not 'text' then it is guaranteed that `children` exists
if (!('children' in tree)) throw new Error('unreachable')

const children = tree.children.map((child) => simpleRender(child, allowed))
if (tree.name !== undefined && allowed.includes(tree.name)) {
return createElement(tree.name, {}, ...children)
} else {
return createElement(Fragment, {}, ...children)
}
}

export default ({ text = '' }): JSX.Element => {
// case where we should italicize the whole clue
if (text.startsWith('""') && text.endsWith('""')) {
return createElement('i', {}, text.slice(1, -1))
}

// fast path for text with no HTML and no entities
if (!text.match(/[<>]|&[^;]+;/)) return createElement('span', {}, text)

// otherwise, parse HTML and render allowed elements
const allowed = ['em', 'strong', 'u', 'i', 'b', 'sup', 'sub']
const tree = simpleParse(text)
return createElement('span', {}, simpleRender(tree, allowed))
}

1 comment on commit 56c5686

@vercel
Copy link

@vercel vercel bot commented on 56c5686 Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.