Skip to content

Instantly share code, notes, and snippets.

@kmelve
Created May 24, 2024 11:10
Show Gist options
  • Save kmelve/a90e0bb2a0c8a806dd35e7e368ca38e9 to your computer and use it in GitHub Desktop.
Save kmelve/a90e0bb2a0c8a806dd35e7e368ca38e9 to your computer and use it in GitHub Desktop.
Markdown to Portable Text paste handler
import { toPlainText } from "@portabletext/react"
import { BlockEditor as DefaultBlockEditor } from "sanity"
import { handlePaste } from "~/studio/components/blockEditor/handlePaste"
const wordsPerMinute = 200
export default function BlockEditor(props: any, ref) {
const value = props.value ?? []
const plainText = toPlainText(value)
const characterCount = plainText.length
const wordCount = plainText.split(/\s+/g).filter(Boolean).length
const readingTime = Math.ceil(wordCount / wordsPerMinute)
return (
<div style={{ display: "grid" }}>
<DefaultBlockEditor ref={ref} {...props} onPaste={handlePaste} />
<div>
<div>
<span>🔠</span>
{characterCount}
</div>
<div>
<span>🚾</span>
{wordCount}
</div>
<div>
<span>⏱️</span>
{readingTime} min
</div>
</div>
</div>
)
}
import { htmlToBlocks } from "@sanity/block-tools"
import { micromark } from "micromark"
interface Input {
event: ClipboardEvent
schemaTypes: SchemaTypes
path: Array<any>
}
interface SchemaTypes {
blockObjects: Array<{ name: string }>
portableText: any
}
interface Block {
_type: "code"
code: string
language: string
}
interface InsertPatch {
insert: ReturnType<typeof htmlToBlocks>
path: Array<any>
}
export async function handlePaste(
input: Input
): Promise<InsertPatch | undefined> {
const { event, schemaTypes, path } = input
const text = event.clipboardData.getData("text/plain")
const json = event.clipboardData.getData("application/json")
if (text && !json) {
const html = micromark(text)
return html
? convertHtmlToSanityPortableTextPatch(html, schemaTypes, path)
: undefined
}
return undefined
}
function convertHtmlToSanityPortableTextPatch(
html: string,
schemaTypes: SchemaTypes,
path: Array<any>
): InsertPatch | undefined {
if (!isCodeTypeAvailable(schemaTypes) || !html) return undefined
const blocks = htmlToBlocks(html, schemaTypes.portableText, {
rules: [{ deserialize: deserializeCodeBlockElement }],
})
return blocks ? { insert: blocks, path } : undefined
}
function isCodeTypeAvailable(schemaTypes: SchemaTypes): boolean {
const hasCodeType = schemaTypes.blockObjects.some(
(type) => type.name === "code"
)
if (!hasCodeType) {
console.warn(
'Run `sanity install @sanity/code-input` and add `type: "code"` to your schema.'
)
}
return hasCodeType
}
function deserializeCodeBlockElement(
el: Element,
next: any,
block: (block: any) => any
): Block | undefined {
if (!isPreformattedText(el)) return undefined
const codeElement = el.children[0]
const childNodes = getCodeChildNodes(el, codeElement as Element)
const codeText = extractTextFromNodes(childNodes)
const language = mapLanguageAliasToActualLanguage(
getLanguageAlias(codeElement as Element)
)
return block({
_type: "code",
code: codeText,
language,
})
}
function isPreformattedText(el: Element): boolean {
return el && el.children && el.tagName && el.tagName.toLowerCase() === "pre"
}
function getCodeChildNodes(
el: Element,
codeElement: Element
): NodeListOf<ChildNode> {
return codeElement && codeElement.tagName.toLowerCase() === "code"
? codeElement.childNodes
: el.childNodes
}
function extractTextFromNodes(childNodes: NodeListOf<ChildNode>): string {
return Array.from(childNodes)
.map((node) => node.textContent || "")
.join("")
}
function getLanguageAlias(codeElement: Element): string {
return codeElement.className.replace("language-", "")
}
function mapLanguageAliasToActualLanguage(languageAlias: string): string {
const languageMapping = {
js: "javascript",
ts: "typescript",
}
return languageMapping[languageAlias] || languageAlias
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment