@@ -21,6 +21,7 @@ import io.wdsj.asw.bukkit.util.context.SignContextEntry
2121import io.wdsj.asw.bukkit.util.context.SignContextTarget
2222import io.wdsj.asw.bukkit.util.message.MessageUtils
2323import net.kyori.adventure.text.Component
24+ import net.kyori.adventure.text.TextReplacementConfig
2425import net.kyori.adventure.text.event.ClickEvent
2526import net.kyori.adventure.text.event.HoverEvent
2627import net.kyori.adventure.text.format.NamedTextColor
@@ -56,9 +57,21 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
5657
5758 val startTime = System .currentTimeMillis()
5859 val lineScan = censorSingleLines(event, player, options)
60+ val multiLineViolation = if (lineScan.violation == null || ! isCancelMode(options)) {
61+ censorMultiLine(event, options)
62+ } else {
63+ null
64+ }
65+ val contextViolation = if (
66+ ! isCancelMode(options) || (lineScan.violation == null && multiLineViolation == null )
67+ ) {
68+ censorContext(event, player, options)
69+ } else {
70+ null
71+ }
5972 val violation = lineScan.violation
60- ? : censorMultiLine(event, lineScan, options)
61- ? : censorContext(event, player, options)
73+ ? : multiLineViolation
74+ ? : contextViolation
6275 ? : return
6376
6477 if (isCancelMode(options) && ! violation.context &&
@@ -111,20 +124,15 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
111124
112125 private fun censorSingleLines (event : SignChangeEvent , player : Player , options : PlayerOptionView ): SignLineScan {
113126 var violation: SignViolation ? = null
114- val cleanLineIndexes = mutableListOf<Int >()
115- val cleanLineContent = StringBuilder ()
116127
117128 for (lineIndex in event.lines().indices) {
118129 val originalComponent = event.line(lineIndex) ? : continue
119- val originalMessage = preprocess(MessageUtils .plainText(originalComponent))
130+ val scanComponent = preprocess(originalComponent)
131+ val originalMessage = MessageUtils .plainText(scanComponent)
120132 val censoredWords = AdvancedSensitiveWords .findAllSensitive(originalMessage)
121133 SensitiveFilterEvents .post(event.isAsynchronous, ModuleType .SIGN , player, originalMessage, censoredWords)
122134
123135 if (censoredWords.isEmpty()) {
124- if (originalMessage.trim().isNotEmpty()) {
125- cleanLineIndexes.add(lineIndex)
126- cleanLineContent.append(originalMessage)
127- }
128136 continue
129137 }
130138
@@ -134,27 +142,41 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
134142 continue
135143 }
136144 val processedMessage = AdvancedSensitiveWords .replaceSensitive(originalMessage)
137- event.line(lineIndex, MessageUtils .replaceLiteral(originalComponent , originalMessage, processedMessage))
145+ event.line(lineIndex, replaceWholeLine(scanComponent , originalMessage, processedMessage))
138146 }
139147
140- return SignLineScan (violation, cleanLineIndexes, cleanLineContent.toString() )
148+ return SignLineScan (violation)
141149 }
142150
143- private fun censorMultiLine (event : SignChangeEvent , lineScan : SignLineScan , options : PlayerOptionView ): SignViolation ? {
151+ private fun censorMultiLine (event : SignChangeEvent , options : PlayerOptionView ): SignViolation ? {
144152 if (! options.bool(PlayerOptions .SIGN_MULTI_LINE_CHECK , PluginSettings .SIGN_MULTI_LINE_CHECK )) return null
145- if (lineScan.cleanLineIndexes.isEmpty()) return null
146153
147- val originalContent = lineScan.cleanLineContent
154+ val lines = event.lines().indices.mapNotNull { lineIndex ->
155+ val component = event.line(lineIndex) ? : return @mapNotNull null
156+ val scanComponent = preprocess(component)
157+ val content = MessageUtils .plainText(scanComponent)
158+ if (content.isBlank()) return @mapNotNull null
159+ SignLineContent (lineIndex, scanComponent, content)
160+ }
161+ if (lines.size < 2 ) return null
162+
163+ val originalContent = lines.joinToString(" " ) { it.content }
148164 val censoredWords = AdvancedSensitiveWords .findAllSensitive(originalContent)
149165 SensitiveFilterEvents .post(event.isAsynchronous, ModuleType .SIGN , event.player, originalContent, censoredWords)
150166 if (censoredWords.isEmpty()) return null
151167
152168 if (isCancelMode(options)) {
153169 event.isCancelled = true
154170 } else {
155- val processedMessage = AdvancedSensitiveWords .replaceSensitive(originalContent)
156- for (lineIndex in lineScan.cleanLineIndexes) {
157- event.line(lineIndex, MessageUtils .plainTextComponent(processedMessage))
171+ val processedLines = resolveSegmentReplacements(
172+ lines.map { it.content.length },
173+ originalContent,
174+ )
175+ lines.forEachIndexed { index, line ->
176+ event.line(
177+ line.index,
178+ replaceWholeLine(line.component, line.content, processedLines[index]),
179+ )
158180 }
159181 }
160182
@@ -181,7 +203,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
181203 }
182204
183205 private fun contextEntry (event : SignChangeEvent ): SignContextEntry {
184- val lines = event.lines().map { preprocess( MessageUtils .plainText(it)) }
206+ val lines = event.lines().map { MessageUtils .plainText(preprocess (it)) }
185207 return SignContextEntry (
186208 content = lines.joinToString(" " ),
187209 target = SignContextTarget (
@@ -191,7 +213,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
191213 event.block.z,
192214 event.side,
193215 ),
194- lineLengths = lines.map( String ::length) ,
216+ lineContents = lines,
195217 )
196218 }
197219
@@ -219,81 +241,98 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
219241 }
220242 }
221243
222- private fun applyEventReplacement (event : SignChangeEvent , entry : SignContextEntry , replacement : String ) {
223- splitLines(entry.lineLengths, replacement).forEachIndexed { index, line ->
224- event.line(index, Component .text(line))
244+ private fun applyEventReplacement (event : SignChangeEvent , entry : SignContextEntry , replacement : List <String >) {
245+ replacement.forEachIndexed { index, line ->
246+ val component = preprocess(event.line(index) ? : Component .empty())
247+ val originalLine = entry.lineContents.getOrElse(index) { " " }
248+ event.line(index, replaceWholeLine(component, originalLine, line))
225249 }
226250 }
227251
228- private fun scheduleSignMutation (entry : SignContextEntry , replacement : String? ) {
252+ private fun scheduleSignMutation (entry : SignContextEntry , replacement : List < String > ? ) {
229253 val world = Bukkit .getWorld(entry.target.worldId) ? : return
230254 val location = Location (world, entry.target.x.toDouble(), entry.target.y.toDouble(), entry.target.z.toDouble())
231255 AdvancedSensitiveWords .getScheduler().runTaskLater(location, Runnable {
232256 val sign = location.block.state as ? Sign ? : return @Runnable
233257 val signSide = sign.getSide(entry.target.side)
234- val currentContent = (0 until 4 ).joinToString(" " ) { line ->
235- preprocess(MessageUtils .plainText(signSide.line(line)))
236- }
258+ val currentLines = (0 until 4 ).map { line -> preprocess(signSide.line(line)) }
259+ val currentContent = currentLines.joinToString(" " ) { MessageUtils .plainText(it) }
237260 if (currentContent != entry.content) return @Runnable
238261
239- val lines = replacement?.let { splitLines(entry.lineLengths, it) } ? : List (4 ) { " " }
240- lines.forEachIndexed { index, line -> signSide.line(index, Component .text(line)) }
262+ val lines = replacement ? : List (4 ) { " " }
263+ lines.forEachIndexed { index, line ->
264+ val component = if (replacement == null ) {
265+ Component .empty()
266+ } else {
267+ replaceWholeLine(currentLines[index], entry.lineContents[index], line)
268+ }
269+ signSide.line(index, component)
270+ }
241271 sign.update(false , false )
242272 }, 1L )
243273 }
244274
275+ private fun resolveSegmentReplacements (segmentLengths : List <Int >, context : String ): List <String > {
276+ return SignTextLayout .replaceSegments(segmentLengths, context, replacementSpans(context))
277+ }
278+
245279 private fun resolveContext (entries : List <SignContextEntry >, context : String ): ContextResolution {
246- val starts = IntArray (entries.size)
247- for (index in 1 until entries.size) {
248- starts[index] = starts[index - 1 ] + entries[index - 1 ].content.length
280+ val entryLengths = IntArray (entries.size) { entries[it].content.length }
281+ val entryStarts = segmentStarts(entryLengths)
282+ val lineReferences = buildList {
283+ entries.forEachIndexed { entryIndex, entry ->
284+ entry.lineContents.forEachIndexed { lineIndex, content ->
285+ if (content.isNotEmpty()) {
286+ add(ContextLineReference (entryIndex, lineIndex, content))
287+ }
288+ }
289+ }
290+ }
291+ val spans = replacementSpans(context)
292+ val resolvedLines = SignTextLayout .replaceSegments(
293+ lineReferences.map { it.content.length },
294+ context,
295+ spans,
296+ )
297+ val replacements = entries.associateWith {
298+ MutableList (it.lineContents.size) { " " }
299+ }
300+ lineReferences.forEachIndexed { index, reference ->
301+ replacements.getValue(entries[reference.entryIndex])[reference.lineIndex] = resolvedLines[index]
249302 }
250303
251- val replacements = Array (entries.size) { StringBuilder () }
252304 val affectedEntries = linkedSetOf<SignContextEntry >()
305+ spans.forEach { span ->
306+ markAffectedEntries(affectedEntries, entries, entryStarts, span.start, span.end)
307+ }
308+
309+ return ContextResolution (
310+ replacements.mapValues { it.value.toList() },
311+ affectedEntries,
312+ )
313+ }
314+
315+ private fun replacementSpans (context : String ): List <SignReplacementSpan > {
316+ val spans = mutableListOf<SignReplacementSpan >()
253317 val results = AdvancedSensitiveWords .findAllSensitiveRaw(context)
254318 .sortedWith(compareBy<IWordResult > { it.startIndex() }.thenByDescending { it.endIndex() })
255319 var cursor = 0
256320 for (result in results) {
257321 val start = result.startIndex().coerceIn(0 , context.length)
258322 val end = result.endIndex().coerceIn(start, context.length)
259323 if (start < cursor || start == end) continue
260-
261- appendUnchangedContext(replacements, entries, starts, context, cursor, start)
262- replacements[entryIndexAt(starts, start)].append(replacementFor(context, result))
263- markAffectedEntries(affectedEntries, entries, starts, start, end)
324+ spans.add(SignReplacementSpan (start, end, replacementFor(context, start, end)))
264325 cursor = end
265326 }
266- appendUnchangedContext(replacements, entries, starts, context, cursor, context.length)
267-
268- return ContextResolution (
269- entries.indices.associate { index -> entries[index] to replacements[index].toString() },
270- affectedEntries,
271- )
327+ return spans
272328 }
273329
274- private fun appendUnchangedContext (
275- replacements : Array <StringBuilder >,
276- entries : List <SignContextEntry >,
277- starts : IntArray ,
278- context : String ,
279- start : Int ,
280- end : Int ,
281- ) {
282- var cursor = start
283- while (cursor < end) {
284- val entryIndex = entryIndexAt(starts, cursor)
285- val entryEnd = starts[entryIndex] + entries[entryIndex].content.length
286- val segmentEnd = minOf(end, entryEnd)
287- replacements[entryIndex].append(context, cursor, segmentEnd)
288- cursor = segmentEnd
330+ private fun segmentStarts (lengths : IntArray ): IntArray {
331+ val starts = IntArray (lengths.size)
332+ for (index in 1 until lengths.size) {
333+ starts[index] = starts[index - 1 ] + lengths[index - 1 ]
289334 }
290- }
291-
292- private fun entryIndexAt (starts : IntArray , index : Int ): Int {
293- for (entryIndex in starts.indices.reversed()) {
294- if (index >= starts[entryIndex]) return entryIndex
295- }
296- return 0
335+ return starts
297336 }
298337
299338 private fun markAffectedEntries (
@@ -312,36 +351,34 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
312351 }
313352 }
314353
315- private fun replacementFor (context : String , result : IWordResult ): String {
316- val sensitiveWord = context.substring(result.startIndex(), result.endIndex() )
354+ private fun replacementFor (context : String , start : Int , end : Int ): String {
355+ val sensitiveWord = context.substring(start, end )
317356 configuration.get(PluginSettings .DEFINED_REPLACEMENT ).forEach { definition ->
318357 val separator = definition.indexOf(' |' )
319358 if (separator <= 0 || definition.indexOf(' |' , separator + 1 ) >= 0 ) return @forEach
320359 if (definition.substring(0 , separator) == sensitiveWord) {
321360 return definition.substring(separator + 1 )
322361 }
323362 }
324- return configuration.get(PluginSettings .REPLACEMENT ).repeat(result.endIndex() - result.startIndex() )
363+ return configuration.get(PluginSettings .REPLACEMENT ).repeat(end - start )
325364 }
326365
327- private fun splitLines (lineLengths : List <Int >, content : String ): List <String > {
328- val lines = MutableList (4 ) { " " }
329- var offset = 0
330- for (lineIndex in lines.indices) {
331- val expectedLength = lineLengths.getOrElse(lineIndex) { 0 }
332- val end = minOf(content.length, offset + expectedLength)
333- lines[lineIndex] = content.substring(offset, end)
334- offset = end
335- }
336- if (offset < content.length) {
337- lines[3 ] + = content.substring(offset)
338- }
339- return lines
366+ private fun replaceWholeLine (component : Component , originalText : String , replacement : String ): Component {
367+ if (originalText == replacement) return component
368+
369+ val replaced = MessageUtils .replaceLiteral(component, originalText, replacement)
370+ if (MessageUtils .plainText(replaced) == replacement) return replaced
371+ return Component .text(replacement).style(component.style())
340372 }
341373
342- private fun preprocess (text : String ): String {
343- if (! configuration.get(PluginSettings .PRE_PROCESS )) return text
344- return text.replace(Utils .preProcessRegex.toRegex(), " " )
374+ private fun preprocess (component : Component ): Component {
375+ if (! configuration.get(PluginSettings .PRE_PROCESS )) return component
376+
377+ val replacementConfig = TextReplacementConfig .builder()
378+ .match(Utils .preProcessRegex.toPattern())
379+ .replacement(" " )
380+ .build()
381+ return component.replaceText(replacementConfig)
345382 }
346383
347384 private fun isCancelMode (options : PlayerOptionView ): Boolean {
@@ -350,8 +387,18 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
350387
351388 private data class SignLineScan (
352389 val violation : SignViolation ? ,
353- val cleanLineIndexes : List <Int >,
354- val cleanLineContent : String ,
390+ )
391+
392+ private data class SignLineContent (
393+ val index : Int ,
394+ val component : Component ,
395+ val content : String ,
396+ )
397+
398+ private data class ContextLineReference (
399+ val entryIndex : Int ,
400+ val lineIndex : Int ,
401+ val content : String ,
355402 )
356403
357404 private data class SignViolation (
@@ -361,7 +408,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
361408 )
362409
363410 private data class ContextResolution (
364- val replacements : Map <SignContextEntry , String >,
411+ val replacements : Map <SignContextEntry , List < String > >,
365412 val affectedEntries : Set <SignContextEntry >,
366413 )
367414}
0 commit comments