Skip to content

Commit

Permalink
revise word replace (#34)
Browse files Browse the repository at this point in the history
* modify word replace for case-sensitivity and word boundaries

* sanitize user-defined word replacements

* revise early-returns

* prioritize word replacements by length

* optimize sorting

* Revert "optimize sorting"

This reverts commit d9bb063.

* adjust regex for word boundaries

* adjust regex to remove word boundaries

* modify word replace to remove case-sensitivity

* create switches "Match whole word" and "Match case"

* implement matching options

* handle conflicts in word replace

* lint check

* (wip) rework word replace system with new constraints from pr

uses a single object instead of 2. there is probably bugs. this is a wip

* add rule check, remove unnecessary code, separate objects

---------

Co-authored-by: nae <[email protected]>
  • Loading branch information
fuwako and naeruru authored Mar 26, 2024
1 parent 99b4bca commit 14082e3
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 12 deletions.
1 change: 1 addition & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default {
settingsStore,
connectionStore,
is_electron,
wordReplaceStore,
}
},
unmounted() {
Expand Down
62 changes: 54 additions & 8 deletions src/components/settings/WordReplace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,42 @@
</template>
</v-list-item>
</v-card>
<v-row class="mt-6">
<v-col :cols="12" :sm="6">
<v-card flat>
<v-list-item :title="$t('settings.word_replace.match_whole_word')">
<template #append>
<v-switch
v-model="wordReplaceStore.match_whole_word"
color="primary"
hide-details
inset
/>
</template>
</v-list-item>
</v-card>
</v-col>
<v-col :cols="12" :sm="6">
<v-card flat>
<v-list-item :title="$t('settings.word_replace.match_case')">
<template #append>
<v-switch
v-model="wordReplaceStore.match_case"
color="primary"
hide-details
inset
/>
</template>
</v-list-item>
</v-card>
</v-col>
</v-row>
<div v-if="replacements.length" class="mt-6">
<v-row v-for="(replacement, i) in replacements">
<v-col :cols="12" :sm="6">
<v-text-field v-model="replacement.replacing" :label="$t('settings.word_replace.replacing')" append-icon="mdi-arrow-right-bold" hide-details />
<v-col :cols="12" :sm="6" class="pt-1 pb-0">
<v-text-field v-model="replacement.replacing" :label="$t('settings.word_replace.replacing')" :rules="[exists]" append-icon="mdi-arrow-right-bold" />
</v-col>
<v-col :cols="10" :sm="6">
<v-col :cols="10" :sm="6" class="pt-1 pb-0">
<v-text-field v-model="replacement.replacement" :label="$t('settings.word_replace.replacement')" hide-details>
<template #append>
<v-btn size="x-small" color="red" icon="mdi-minus" @click="remove_entry(i)" />
Expand Down Expand Up @@ -57,28 +87,44 @@ export default {
}),
unmounted() {
this.wordReplaceStore.word_replacements = {}
this.replacements.forEach((entry) => {
this.wordReplaceStore.word_replacements[entry.replacing.toLowerCase()] = entry.replacement
this.replacements
.sort((a, b) => a.replacing.localeCompare(b.replacing)) // Sort keys by locale (e.g., alphabetical sort). This is cosmetic.
.sort((a, b) => b.replacing.length - a.replacing.length) // Sort keys by string length: longer strings to shorter strings.
.forEach((entry) => {
this.wordReplaceStore.word_replacements[entry.replacing] = entry.replacement
})
this.wordReplaceStore.word_replacements_lowercase = {}
Object.keys(this.wordReplaceStore.word_replacements).forEach((key) => {
const keyLowerCase = key.toLowerCase()
// First-come, first-served.
// For example, the replacement keys "Hello" and "hello" are both transformed to lowercase as "hello".
// When ordered by locale, their order is ["hello", "Hello"]. The replacement entry for "hello" will be used in case-insensitive replacements.
if (!this.wordReplaceStore.word_replacements_lowercase[keyLowerCase])
this.wordReplaceStore.word_replacements_lowercase[keyLowerCase] = this.wordReplaceStore.word_replacements[key]
})
},
mounted() {
this.replacements = Object.entries(this.wordReplaceStore.word_replacements).map(([replacing, replacement]) => ({
replacing,
replacement,
}))
// console.log(this.replacements)
},
methods: {
add_entry() {
// this.wordReplaceStore.word_replacements[""] = ""
this.replacements.push({
replacing: '',
replacement: '',
})
},
remove_entry(i: number) {
this.replacements.splice(i, 1)
// delete this.wordReplaceStore.word_replacements[this.replacement_list[i].replacing]
},
exists(value: string) {
return !value || this.replacements.filter((e: any) => value === e.replacing).length < 2 || `${value} already exists`
},
},
}
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/localization/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export default {
title: 'Word Replace',
description: 'Add words or phrases to replace here',
enabled: 'Enable replacing words or phrases',
match_whole_word: 'Match whole word only',
match_case: 'Match case',
info: 'Use the + button to add a new replacement!',
replacing: 'Replacing',
replacement: 'Replacement',
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/localization/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export default {
title: 'Sustitución de palabras',
description: 'Agregue palabras o frases para reemplazar aquí',
enabled: 'Permitir reemplazar palabras o frases',
match_whole_word: 'Sólo palabras completas',
match_case: 'Coincidir mayúsculas/minúsculas',
info: 'Utilice el botón + para agregar un nuevo reemplazo.',
replacing: 'Reemplazando ',
replacement: 'Reemplazo',
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/localization/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export default {
title: 'テキストリプレース',
description: 'ここで置き換えるテキストを追加します',
enabled: 'テキストリプレース',
match_whole_word: '単語単位',
match_case: '大文字/小文字を区別',
info: '新しい置き換えを追加する場合は「+」バタンを使用してください',
replacing: 'リプレース',
replacement: 'リプレースメント',
Expand Down
7 changes: 6 additions & 1 deletion src/stores/speech.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const useSpeechStore = defineStore('speech', {
speech.speak(input)
},
async on_submit(log: any, index: number) {
if (!log.transcript.replace(/\s/g, '').length)
if (!log.transcript.trim()) // If the submitted input is only whitespace, do nothing. This may occur if the user only submitted whitespace.
return

const logStore = useLogStore()
Expand All @@ -124,6 +124,11 @@ export const useSpeechStore = defineStore('speech', {

// word replace
log.transcript = replace_words(log.transcript)
if (!log.transcript.trim()) { // If the processed input is only whitespace, do nothing. This may occur if the entire log transcript was replaced with whitespace.
logStore.loading_result = false

return
}

// scroll to bottom
const loglist = document.getElementById('loglist')
Expand Down
50 changes: 47 additions & 3 deletions src/stores/word_replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,62 @@ interface word_replacements {
export const useWordReplaceStore = defineStore('wordreplace', {
state: () => ({
enabled: true,
match_whole_word: false,
match_case: false,
word_replacements: {} as word_replacements,
word_replacements_lowercase: {} as word_replacements,
}),
getters: {

},
actions: {
// Escape regex metacharacters
escapeRegExp(input: string) {
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
},
// Add case insensitivity
case_insensitive_regex(input: string) {
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
},
// Replace words.
// Longer keys have higher priority (e.g., "Hello world" → "apple", "Hello" → "banana".
// The transcript "Hello world" will become "apple"). Keys are sorted by the Word Replace component.
replace_words(input: string): string {
if (!this.enabled || !Object.keys(this.word_replacements).length)
return input
const replace_re = new RegExp(Object.keys(this.word_replacements).join('|'), 'gi')
return input.replace(replace_re, (matched) => {
return this.word_replacements[matched.toLowerCase()]

let joined_keys: string[] = []
let input_interpretation: string

// Interpret depending on the "Match case" option.
if (!this.match_case) {
input_interpretation = input.toLowerCase()
joined_keys = Object.keys(this.word_replacements_lowercase)
}
else {
input_interpretation = input
joined_keys = Object.keys(this.word_replacements)
}

const regex_keys = joined_keys.map(key => this.escapeRegExp(key)).join('|')

// Build pattern depending on options.
let pattern

// Option: Word/phrase match. Word boundaries prevent unexpected replacements
// (e.g, "script" → "HELLO" would undesirably cause "description" → "deHELLOion").
if (!this.match_whole_word)
pattern = regex_keys
else
pattern = `(?<![\\w*])(${regex_keys})(?![\\w*])`

const replace_re = new RegExp(pattern, 'g')

return input_interpretation.replace(replace_re, (matched) => {
if (!this.match_case)
return this.word_replacements_lowercase[matched.toLowerCase()]
else
return this.word_replacements[matched]
})
},
},
Expand Down

0 comments on commit 14082e3

Please sign in to comment.