Skip to content

Instantly share code, notes, and snippets.

@SimeonGriggs
Created July 15, 2021 14:25
Show Gist options
  • Save SimeonGriggs/44929f1ba5fbde86ef38fe3f64659b38 to your computer and use it in GitHub Desktop.
Save SimeonGriggs/44929f1ba5fbde86ef38fe3f64659b38 to your computer and use it in GitHub Desktop.
Sanity.io Document Action to automate Annotations from a 'Vocabulary' schema
import {useState} from 'react'
import {randomKey} from '@sanity/util/content'
import sanityClient from 'part:@sanity/base/client'
const apiVersion = `2021-05-19`
const client = sanityClient.withConfig({apiVersion})
export default function AnnotateAction({draft, published}) {
const [isAnnotating, setIsAnnotating] = useState(false)
const doc = draft || published
async function performAnnotation() {
setIsAnnotating(true)
const {content} = doc
// 1. Map over every paragraph (aka "block")
// 2. Extract every piece of text
// 3. Filter that text's words down to any word OVER three characters
// 4. Query the `vocabulary` schema for matching words
// 5. Then create a markDef (annotation) to create any matching references
const contentWithAnnotations = await Promise.all(
content.map(async (block) => {
if (!block?.children?.length) {
return block
}
const newMarkDefs = block?.markDefs?.length ? [...block.markDefs] : []
const newChildren = await block.children.reduce(async (allChildren, child) => {
// Only query `span` text with no existing marks
if (child._type !== 'span' || !child?.text || child.marks.length) {
return [...(await allChildren), child]
}
// Create the query params
const words = child.text
.split(' ')
.filter((word) => word.length > 3)
.map((word) => word.toLowerCase())
if (!words?.length) {
return [...(await allChildren), child]
}
// Query the words in this span `text` against the vocabulary schema
const query = `*[_type == "vocabulary" && lower(word) in $words]{ _id, word }`
const matches = await client.fetch(query, {words})
// No matches, just return the child
if (!matches?.length) {
return [...(await allChildren), child]
}
// Separate multiple unmatched words from matches ones,
// and turn them into reference annotations
const separateTextFromMatches = child.text.split(' ').reduce((acc, word, index) => {
const lastIdx = acc.length - 1
const spanKey = randomKey(12)
const markKey = randomKey(12)
// Word matches the vocabulary query, setup a marksDef
const match = matches.find(
// eslint-disable-next-line max-nested-callbacks
(refMatch) => refMatch.word.toLowerCase() === word.toLowerCase()
)
if (match) {
// console.log(`word match: `, word)
// Add a space before, if it's not the first word
if (index) {
acc.push({
_type: `span`,
_key: randomKey(12),
text: ` `,
marks: [],
})
}
// Add our new annotation...
acc.push({
_type: `span`,
_key: spanKey,
text: word,
marks: [markKey],
})
// ...and its markDef to the outer block
newMarkDefs.push({
_key: markKey,
_type: `vocabulary`,
vocabularyReference: {
_ref: match._id,
_type: `reference`,
},
})
} else if (acc[lastIdx]?.text && !acc[lastIdx]?.marks?.length) {
// console.log(`word add: `, word)
// Last item was a word, so add this word onto it
// Re-split the text and then join again (to prevent double spaces)
acc[lastIdx].text = [...acc[lastIdx].text.split(' '), word].join(' ')
} else {
// console.log(`word new: `, word)
// Word is just a word, add it to the last one or start a new one
acc.push({
_type: `span`,
_key: spanKey,
text: index ? ` ${word}` : word,
marks: [],
})
}
return acc
}, [])
return [...(await allChildren), ...separateTextFromMatches]
}, [])
// Compile the new content
return {...block, children: newChildren, markDefs: newMarkDefs}
})
)
// In this example we send the new annotated text to a different field
client
.patch(doc._id)
.set({contentProcessed: contentWithAnnotations})
.commit()
.then(() => setIsAnnotating(false))
.catch((error) => {
console.error(error)
setIsAnnotating(false)
})
}
return {
label: isAnnotating ? 'Annotating...' : 'Annotate',
onHandle: () => performAnnotation(),
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment