mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 14:49:10 +03:00
* 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>
256 lines
6.5 KiB
TypeScript
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,
|
|
}
|
|
}
|