Die Textshine Bridge ist ein API um von einer Website aus Textshine steuern zu können. Achtung: Die Textshine-Browser-Extension muss beim Nutzer installiert sein, ansonsten wird ein Error gethrowed. Der primäre usecase aktuell ist es eine Korrektion aus einer CMS Integration zu starten, in Zukunft könnten weitere Funktionen folgen.
npm install textshine-browser-extension-bridge
Entweder direkter Import des SDKs (vermeidet global scope pollution):
// ESM-Import (preferred)
import { startCorrection } from 'textshine-browser-extension-bridge'
// ODER CommonJS-Import
const { startCorrection } = require('textshine-browser-extension-bridge');
// Nutzung
startCorrection(params: { type, src }, callback)
oder register script welches das API ins window objekt registriert (Achtung, funktioniert nicht bei SSR weil es window-Existenz voraussetzt):
// ESM (preferred)
import 'textshine-browser-extension-bridge/register'
// or CommonJS
require('textshine-browser-extension-bridge/register');
window.textshineBridge.startCorrection(params: { type, src }, callback)
Sollte kein buildprozess existieren lässt sich auch die Datei node_modules/textshine-browser-extension-bridge/dist/browser/textshine-bridge.min.js
kopieren und mittels vanilla html script tag einbetten.
Nach Installation der Extension exposed diese ein API zur Nutzung innerhalb einer Website.
Signatur:
type TextshineBridgeCorrectionType = 'html' | 'plaintext'
type TextshineBridgeCorrectionPayload = {
type: TextshineBridgeCorrectionType
src: string
}
window.textshineBridge.startCorrection: (params: TextshineBridgeCorrectionPayload, callback: (result: string) => void) => void
Beispiel:
import { startCorrection } from 'textshine-browser-extension-bridge'
const type = 'html'
const src = 'ein gültiger HTML string'
const callback = (result) => someElement.innerHTML = result // result ist das korrigierte HTML
startCorrection(params: { type, src }, callback)
Bei CMS/Editoren bei denen sich der gesamte Content gemeinsam ausgeben und wieder ersetzen lässt, muss dieser lediglich als Ganzes in HTML serialisiert und ggbfls. deserialisiert werden.
// wenn html direkt supported wird
const src = getHTMLFromCMS()
const callback = (result) => setHTMLIntoCMS(result)
startCorrection(params: { type: 'html', src }, callback)
// wenn serialisiert und deserialisiert werden muss
const originalSrc = getDataFromCMS()
const src = convertDataToHTML(originalSrc)
const callback = (result) => setDataIntoCMS(convertHTMLToData(result))
startCorrection(params: { type: 'html', src }, callback)
Bei CMS/Editoren die block-basiert sind wie Wordpress Gutenberg, bei denen einzelne Blöcke und nicht ein gesamtes Dokument ersetzt werden sollen, muss ein präziserer import/export stattfinden:
- Ein Importer liest alle Blöcke über das CMS native API
- Der Importer läuft dann über alle Blöcke und wandelt korrigierbare Blöcke in HTML Fragments um
- Zu jedem Block wird eine ID erstellt um Korrekturen rückführen zu können
- Ein exporter wird erstellt, welcher anhand der IDs im DOM Änderungen schreibt
- Das generierte HTML wird an die Textshine Bridge geschickt mit einem callback des exporters
- Nach Abschluss der Korrektur wird der callback gecalled und der exporter ersetzt Inhalte im DOM
// Recursive function for processing blocks
function processBlocksForImport(blocks: WPBlock[]): { html: string; editableBlocks: WPBlock[] } {
let html = ''
const editableBlocks: WPBlock[] = []
// note that any blocks that are not handled in here are ignored
for (const block of blocks) {
// https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/paragraph/block.json
if (block.name === 'core/paragraph') {
html += `<p data-wp-block-id="${block.clientId}">${block.content || ''}</p>`
editableBlocks.push(block)
}
// https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/heading/block.json
else if (block.name === 'core/heading') {
const level = block.attributes.level || 2
html += `<h${level} data-wp-block-id="${block.clientId}">${block.content || ''}</h${level}>`
editableBlocks.push(block)
}
// https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/list/block.json
else if (block.name === 'core/list') {
// Special handling for lists - no data-wp-block-id attribute
const isOrdered = block.attributes.ordered === true
const listTag = isOrdered ? 'ol' : 'ul'
html += `<${listTag}>`
// Process list items
if (block.innerBlocks.length > 0) {
const result = processBlocksForImport(block.innerBlocks)
html += result.html
editableBlocks.push(...result.editableBlocks)
}
html += `</${listTag}>`
}
// https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/list-item/block.json
else if (block.name === 'core/list-item') {
html += `<li data-wp-block-id="${block.clientId}">${block.content || ''}</li>`
editableBlocks.push(block)
}
// for container blocks we only care about their innerBlocks
// https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/group/block.json
// https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/columns/block.json
// https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/column/block.json
else if (['core/group', 'core/columns', 'core/column'].includes(block.name)) {
// For container blocks, we only care about their innerBlocks
if (block.innerBlocks.length > 0) {
const result = processBlocksForImport(block.innerBlocks)
html += result.html
editableBlocks.push(...result.editableBlocks)
}
}
}
return { html, editableBlocks }
}
// Keep your original exporter function exactly as is
async function exporter(updatedContent: string, blocks: WPBlock[]) {
// prepare updates array
const updates = []
// parse updatedContent html
const parser = new DOMParser()
const doc = parser.parseFromString(updatedContent, 'text/html')
// iterate over all blocks that need to be updated
for (const block of blocks) {
// find the block in the DOM
const blockElement = doc.querySelector(`[data-wp-block-id="${block.clientId}"]`)
// if the block is not found, we fucked up truly, so its fair to throw
if (!blockElement) {
throw new Error('Gutenberg export: block not found')
}
// get the content of the block
const blockContent = blockElement.innerHTML
// push the update
updates.push({
clientId: block.clientId,
type: block.name,
attribute,
value: blockContent
})
}
// send updates to background
if (updates.length > 0) {
for (const blockUpdate of blockUpdates) {
const updateToSet: Record<string, unknown> = {}
updateToSet[blockUpdate.attribute] = blockUpdate.value
window.wp.data.dispatch('core/block-editor').updateBlockAttributes(blockUpdate.clientId, updateToSet)
}
}
}
// bind this importer onto a CMS button click for example
const importer = async () => {
const wpblocks = await window.wp.data.select('core/block-editor').getBlocks()
// most pages wont have wp blocks, so return as early as possible
if (!wpblocks) {
return []
}
// Process blocks recursively starting with all blocks
// This will automatically filter for compatible ones
let { html: combinedHtml, editableBlocks } = processBlocksForImport(wpblocks)
// If no editable blocks were found, return empty array
if (editableBlocks.length === 0) {
return []
}
const callback = (updatedSource: correctedHTML) => {
exporter(updatedSource, editableBlocks)
}
// call the textshine bridge
window.textshineBridge.startCorrect({ type: html, src: combinedHtml }, callback)
}