refactor(stage-ui): replace grapheme cluster reader from clustr (#308)

This commit is contained in:
Makito
2025-07-22 12:26:49 +08:00
committed by GitHub
parent a856140fb9
commit 3af094b6be
@@ -3,9 +3,14 @@
<script setup lang="ts">
import type { Animator } from './animators'
import { readGraphemeClusters } from 'clustr'
import { onMounted, ref, shallowRef, watch } from 'vue'
const props = defineProps<{
/**
* A plain string or a ReadableStream of bytes from text in UTF-8 encoding.
* If a stream is provided, the stream **SHOULD NOT** be reused. (i.e. You should not set a same stream twice.)
*/
text?: string | ReadableStream<Uint8Array>
textClass?: string | string[]
animator?: Animator
@@ -19,65 +24,6 @@ const targets = ref<string[]>([])
const abortController = shallowRef<AbortController>()
const segmenter = new Intl.Segmenter('und', { granularity: 'grapheme' })
function readGraphemeClusters(stream: ReadableStream<Uint8Array>, options?: { signal?: AbortSignal }) {
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
const decoder = new TextDecoder('utf-8', { fatal: false })
const reader = stream.getReader()
const signal = options?.signal
async function* iterator() {
let buf = ''
while (true) {
if (signal?.aborted) {
reader.cancel()
throw new Error('Aborted')
}
const readPromise = reader.read()
let result
if (signal) {
result = await Promise.race([
readPromise,
new Promise((_, reject) => {
signal.addEventListener('abort', () => reject(new Error('Aborted')), { once: true })
}),
])
}
else {
result = await readPromise
}
const { done, value } = result as ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>
if (done) {
const segments = [...segmenter.segment(buf)]
for (const seg of segments) {
yield seg.segment
}
break
}
buf += decoder.decode(value, { stream: true })
const segments = [...segmenter.segment(buf)]
if (segments.length > 1) {
const last = segments.pop()!
for (const seg of segments) {
yield seg.segment
}
buf = buf.slice(last.index)
}
}
}
const asyncIterator = iterator()
return {
iterator: asyncIterator,
[Symbol.asyncIterator]() { return asyncIterator },
}
}
watch(() => props.text, async (text) => {
if (!text)
return
@@ -89,7 +35,7 @@ watch(() => props.text, async (text) => {
abortController.value = new AbortController()
try {
targets.value = []
for await (const cluster of readGraphemeClusters(text, { signal: abortController.value.signal })) {
for await (const cluster of readGraphemeClusters(text.getReader(), { signal: abortController.value.signal })) {
targets.value.push(cluster)
emits('textSplit', cluster)
}