Files
supabase/packages/ui-patterns/SqlToRest/syntax-highlighter/transform-renderer.tsx
T
Greg RichardsonandCharis 3f1df65bba SQL to REST translator (#25978)
* feat: sql-to-rest poc

* feat: sql-to-supabase-js poc

* chore: upgrade to libpg-query@15

* feat: sql-to-rest docs tool

* docs(sql-to-rest): add introduction

* docs: wording improvements

* fix: theme

* chore: tailwind placeholder

* feat(sql-to-rest): limit, offset, and range

* feat(sql-to-rest): sort by

* fix(sql-to-rest): ui theme

* feat(sql-to-rest): casts

* feat(sql-to-rest): dynamic faqs in ui

* feat(sql-to-rest): improve ui responsiveness

* feat(sql-to-rest): more advanced faqs in ui

* feat(sql-to-rest): http and curl syntax highlighting

* feat(sql-to-rest): baseUrl in http/curl rendering

* docs(sql-to-rest): clarification on curl flags

* feat(sql-to-rest): more faqs

* refactor(sql-to-rest): split faqs into own file

* refactor(sql-to-rest): move formatters to sql-to-rest package

* docs(sql-to-rest): move page to new tools section

* feat(sql-to-rest): joins and resource embeddings

* chore(sql-to-rest): rename 'convert' to 'translate'

* feat(sql-to-rest): add faqs

* feat(sql-to-rest): ui improvements

* feat(sql-to-rest): faq title adjustment

* feat(sql-to-rest): link to client lib docs

* feat(sql-to-rest): assumptions

* feat(sql-to-rest): friendly 'not supported yet' errors

* feat(sql-to-rest): json columns

* feat(sql-to-rest): remove curl -G flag if unused

* feat(sql-to-rest): better error handling

* chore: revert accidental rls ai work

* fix: build errors

* fix(sql-to-rest): theme

* feat(sql-to-rest): change base url

* feat(sql-to-rest): basic aggregate poc

* feat(sql-to-rest): group by

* feat(sql-to-rest): more group by checks

* feat(sql-to-rest): count(*) special case

* fix(sql-to-rest): tests

* feat(sql-to-rest): faq on 'and' operator

* fix(sql-to-rest): error messages

* fix(sql-to-rest): multiple filters on same column

* refactor(sql-to-rest): error hints

* feat(sql-to-rest): match and imatch

* feat(sql-to-rest): in operator

* feat(sql-to-rest): parsing error hints

* feat(sql-to-rest): default language to curl

* feat(sql-to-rest): format on cmd+s

* feat(sql-to-rest): format initial value

* fix(sql-to-rest): turn off tab query group

* feat(sql-to-rest): improve wording in error messages

* feat(sql-to-rest): animated code blocks

* feat(sql-to-rest): more error hints

* fix(sql-to-rest): strip primary relation prefix from columns

* feat(sql-to-rest): json column in order by clause

* feat(sql-to-rest): validate all referenced relations exist in from clause

* refactor(sql-to-rest): split core sql-to-rest lib into separate repo

* feat(sql-to-rest): upgrade to v0.1.1

* feat(sql-to-rest): upgrade to v0.1.2

* feat(sql-to-rest): upgrade to v0.1.4

* chore: upgrade libpg-query

* feat: update 'create a pr' link

* fix: app builds

* fix: prettier

* fix: typecheck

* fix: studio tests

* chore: remove console.log

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* docs(sql-to-rest): remove broken link to client libraries

---------

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
2024-05-22 12:37:01 -06:00

256 lines
6.5 KiB
TypeScript

import { LazyMotion, m } from 'framer-motion'
import { PropsWithChildren } from 'react'
import { SyntaxHighlighterProps, createElement } from 'react-syntax-highlighter'
// Make sure to return the specific export containing the feature bundle.
const loadFramerFeatures = () => import('./framer-features').then((res) => res.default)
type RendererElementNode = {
type: 'element'
tagName: keyof JSX.IntrinsicElements | React.ComponentType<any>
properties: { className: any[]; [key: string]: any }
children: RendererNode[]
}
type RendererTextNode = {
type: 'text'
value: string | number
}
type RendererNode = RendererElementNode | RendererTextNode
export type WrapperProps = PropsWithChildren<{
text: string
}>
export type TransformRendererProps = {
search: (text: string) => boolean
wrapper: ({ children, text }: WrapperProps) => JSX.Element
}
/**
* Transforms the output of the syntax highlighter.
*
* It uses the provided `search` function to identify which
* HTML elements contain the desired text. It then wraps those
* elements using the provided `wrapper` React component.
*
* Useful for adding interactions within the code block,
* such as clickable URLs.
*/
export function transformRenderer({
search,
wrapper,
}: TransformRendererProps): SyntaxHighlighterProps['renderer'] {
return ({ rows, stylesheet, useInlineStyles }) => {
const newRows = (rows as RendererNode[]).map((node, i) => {
const element = createElement({
node,
stylesheet,
useInlineStyles,
key: `code-segment-${i}`,
})
if (node.type === 'text') {
return element
}
const line = mergeValues([node])
if (!search(line)) {
return (
<m.div key={line} layoutId={line}>
{element}
</m.div>
)
}
const children = splitSpaceElements(node.children)
let startIndex = -1
while (search(mergeValues(children.slice(startIndex + 1)))) {
startIndex++
}
let endIndex = children.length
while (search(mergeValues(children.slice(startIndex, endIndex - 1)))) {
endIndex--
}
const text = mergeValues(children.slice(startIndex, endIndex))
const nodeChildren: RendererNode[] = []
const wrapperElement = elementToRendererElementNode(wrapper, text)
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (i < startIndex || i >= endIndex) {
nodeChildren.push(child)
} else {
if (i === startIndex) {
nodeChildren.push(wrapperElement)
}
wrapperElement.children.push(child)
}
}
node.children = nodeChildren
const reactElement = createElement({
node,
stylesheet,
useInlineStyles,
key: `code-segment-${i}`,
})
return (
<m.div key={line} layoutId={line}>
{reactElement}
</m.div>
)
})
return <LazyMotion features={loadFramerFeatures}>{newRows}</LazyMotion>
}
}
/**
* Recursively merges text nodes into a single string.
*/
function mergeValues(nodes: RendererNode[]): string {
return nodes
.map((node) => {
const { type } = node
if (type === 'text') {
return node.value.toString()
} else if (type === 'element') {
return mergeValues(node.children)
} else {
throw new Error(`Unknown node type '${type}'`)
}
})
.join('')
}
/**
* Searches for space characters at the start or end of an element's text nodes
* and splits them out into their own element + text node.
*/
function splitSpaceElements(nodes: RendererNode[]) {
const children: RendererNode[] = []
for (const child of nodes) {
const newChild = { ...child }
if (newChild.type === 'element') {
let splitNodes = splitSpaceElements(newChild.children)
if (splitNodes.length === 1) {
children.push({
...newChild,
children: splitNodes,
})
continue
}
const [firstNode] = splitNodes.slice(0, 1)
const [lastNode] = splitNodes.slice(-1)
let newFirstNode: RendererElementNode | undefined = undefined
let newLastNode: RendererElementNode | undefined = undefined
if (firstNode.type === 'text' && firstNode.value.toString().match(/^[ ]+$/)) {
newFirstNode = {
...newChild,
children: [firstNode],
}
splitNodes.shift()
}
if (lastNode.type === 'text' && lastNode.value.toString().match(/^[ ]+$/)) {
newLastNode = {
...newChild,
children: [lastNode],
}
splitNodes.pop()
}
if (newFirstNode) {
children.push(newFirstNode)
}
children.push({
...newChild,
children: splitNodes,
})
if (newLastNode) {
children.push(newLastNode)
}
} else {
const stringValue = newChild.value.toString()
const startNonWhitespaceIndex = stringValue.search(/[^ ]/)
const endNonWhitespaceIndex = stringValue.search(/[^ ][ ]+$/) + 1
if (startNonWhitespaceIndex > 0) {
const whitespaceChild: RendererTextNode = {
...newChild,
value: stringValue.substring(0, startNonWhitespaceIndex),
}
children.push(whitespaceChild)
}
if (startNonWhitespaceIndex > 0 || endNonWhitespaceIndex > 0) {
const nonWhitespaceChild: RendererTextNode = {
...newChild,
value: stringValue.substring(
startNonWhitespaceIndex > 0 ? startNonWhitespaceIndex : 0,
endNonWhitespaceIndex > 0 ? endNonWhitespaceIndex : undefined
),
}
children.push(nonWhitespaceChild)
} else {
children.push(newChild)
}
if (endNonWhitespaceIndex > 0) {
const whitespaceChild: RendererTextNode = {
...newChild,
value: stringValue.substring(endNonWhitespaceIndex),
}
children.push(whitespaceChild)
}
}
}
return children
}
/**
* Converts a JSX element to a RendererElementNode
* that the syntax highlighter can work with.
*/
function elementToRendererElementNode(
element: ({ children, text }: WrapperProps) => JSX.Element,
text: string
): RendererElementNode {
const {
type,
props: { children, className, ...props },
} = element({ children: [], text })
return {
type: 'element',
tagName: type,
properties: {
className: className ? [className] : [],
...props,
},
children,
}
}