Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added regex support #142

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

Conversation

idan-h
Copy link

@idan-h idan-h commented Oct 5, 2024

Tested pr, quite simple, that adds regex support for the ignore, require, and replace functionalities.

It works by adding $regex: to the start of each string, and will else function as usual.

Might be worth adding some example patterns, maybe to the readme or something. Not a must.

An example pattern to insert the word space between numbers:
$regex:(?<=\d)\s+(?=\d) replace with space

An example pattern to insert spaces between numbers:
$regex:(?<=[0-9])(?=[0-9]) replace with

it is also possible to use group references by just typing $1, $2 etc.

idan-h added 2 commits October 5, 2024 02:01
Added regex support for ignore strings, required strings and replacement of notification text
Copy link
Owner

@pilot51 pilot51 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

In addition to the requested code changes, the descriptions for all 3 options should mention how to enable regex. For now we can assume the user knows or can figure out for themselves how to write regex, they just need to be made aware of how to enable it.

Strings:
require_ignore_strings_dialog_msg
tts_text_replace_dialog

"(?i)${Pattern.quote(pair.first)}".toRegex(), pair.second)
val pattern =
if (pair.first.startsWith(Constants.REGEX_PREFIX))
Regex(pair.first.removePrefix(Constants.REGEX_PREFIX))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This crashes when a notification is received if the regex is malformed. It should catch PatternSyntaxException as you did for require/ignore and I would suggest falling back to Pattern.quote. I would also suggest, in all 3 options, validating the regex and showing an error (toast is fine) if it's malformed before allowing the user to save it.

I think we want (?i) or RegexOption.IGNORE_CASE here for consistency with the standard string and ignore/require regex, and since the description says it's case insensitive. Alternatively, update the description to indicate that the regex is case sensitive.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that regex should be always case-sensitive. I added this in strings.
I am not sure how to implement the toast thing, but I fixed the crash

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, case sensitive allows for more flexibility and I'm sure anyone who wants to use regex would appreciate it.
Unfortunately, it would appear you forgot to update the code in both places when you changed the strings to say case sensitive.

To post a toast:

CoroutineScope(Dispatchers.Main).launch {
	Toast.makeText(appContext, R.string.malformed_regex, Toast.LENGTH_LONG).show()
}

@idan-h idan-h requested a review from pilot51 December 10, 2024 19:01
"(?i)${Pattern.quote(pair.first)}".toRegex(), pair.second)
val pattern =
if (pair.first.startsWith(Constants.REGEX_PREFIX))
Regex(pair.first.removePrefix(Constants.REGEX_PREFIX))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, case sensitive allows for more flexibility and I'm sure anyone who wants to use regex would appreciate it.
Unfortunately, it would appear you forgot to update the code in both places when you changed the strings to say case sensitive.

To post a toast:

CoroutineScope(Dispatchers.Main).launch {
	Toast.makeText(appContext, R.string.malformed_regex, Toast.LENGTH_LONG).show()
}

@@ -246,16 +247,30 @@ class Service : NotificationListenerService() {
info.ignoreReasons.add(IgnoreReason.EMPTY_MSG)
}
if (ttsMsg != null) {
fun containsOrMatchesRegex(it: String): Boolean {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really a fan of creating local functions like this. I think moving it to the companion object as an extension would be better:

private fun String.containsOrMatchesRegex(it: String): Boolean {
	return if (it.startsWith(Constants.REGEX_PREFIX)) {
		try {
			Regex(it.removePrefix(Constants.REGEX_PREFIX)).containsMatchIn(this)
		} catch (e: PatternSyntaxException) {
			e.printStackTrace()
			CoroutineScope(Dispatchers.Main).launch {
				Toast.makeText(appContext, R.string.malformed_regex, Toast.LENGTH_LONG).show()
			}
			false
		}
	} else contains(it, true)
}

Usage: ttsMsg.containsOrMatchesRegex(it)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, local functions are there for a reason - they are releveant only to the code using them. We could just duplicate the code and ditch the function, but I think it is less elegant.

Also, the toast should be only when saving the regex no? There is not reason to put it on the service

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, case sensitive allows for more flexibility and I'm sure anyone who wants to use regex would appreciate it.
Unfortunately, it would appear you forgot to update the code in both places when you changed the strings to say case sensitive.

Sorry, but I didn't understand what I forgot, could you please refer me to the file?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, local functions are there for a reason - they are releveant only to the code using them. We could just duplicate the code and ditch the function, but I think it is less elegant.

I used to have that mindset of keeping scope as narrow as possible when I was starting out, though Java didn't allow nested methods, which may contribute to why I think it looks so wrong in Kotlin. Narrow scope is important, but there are some subjective exceptions.
Functions serve more purpose than to reduce duplication. They also improve readability by breaking functions into smaller pieces (also important for unit testing) and reducing deeply nested/indented code. Sometimes that's worth expanding the scope of the function into the class.

However, I do sometimes use the cousin of the nested function, the lambda, which I find more visually acceptable especially if it's small.
Just the first 2 lines would change, like so:

val containsOrMatchesRegex = { it: String ->
	if (it.startsWith(Constants.REGEX_PREFIX))
	...

Usage doesn't change.

Anyway, I won't let this block the PR, but I'll eventually change it to match my code style preference if you leave it.

Also, the toast should be only when saving the regex no? There is not reason to put it on the service

Ideally, yes, as I previously suggested.
This is the lazy option which I will also accept. I just want something that could make the user aware that the regex is bad. I would likely move the validation to on-save after the merge if you don't.

Sorry, but I didn't understand what I forgot, could you please refer me to the file?

My mistake on the text replace regex. I saw the (?i) and didn't pay attention to it being for the plain text matching. The ignore/require regex still needs fixed (see other comment on the line).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that everything has it's time and place, and sometimes nested functions might be the best solution, like in this case.
I will change it to lambda.
You are welcome to refactor if you would like, I have no problem with that. I just think that a code refactor should be backed by a good reason, and I don't really see the benefit here.

I will try to move it on save, but I am a bit short on time so if I get stuck I will use the lazy solution.

I saw, thanks

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a matter of familiarity. I don't think I've seen anyone use a local function before even though I work with other experienced Kotlin devs and there's plenty of opportunity to do so in those much larger projects, so it looks weird to me. Maybe that's because we all come from a Java background and it's almost never mentioned in Kotlin documentation and examples. I haven't given it much thought until now, so I'm looking around for other perspectives from the larger Kotlin community and I may change my mind. Feel free to leave it a local function if you prefer.

On the other hand, Kotlin allows functions and properties at the file level outside of any class, again unlike Java. It felt wrong at first, but thanks to how common it is for Compose, I've gotten used to it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually come from a c# background, I don't really know Kotlin 😄

I rarely use local functions aswell, but on some occasions they are helpful.

return if (it.startsWith(Constants.REGEX_PREFIX))
try {
val pattern = it.removePrefix(Constants.REGEX_PREFIX)
Regex(pattern, RegexOption.IGNORE_CASE)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to remove RegexOption.IGNORE_CASE here to make the regex case sensitive.

@@ -246,16 +247,30 @@ class Service : NotificationListenerService() {
info.ignoreReasons.add(IgnoreReason.EMPTY_MSG)
}
if (ttsMsg != null) {
fun containsOrMatchesRegex(it: String): Boolean {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, local functions are there for a reason - they are releveant only to the code using them. We could just duplicate the code and ditch the function, but I think it is less elegant.

I used to have that mindset of keeping scope as narrow as possible when I was starting out, though Java didn't allow nested methods, which may contribute to why I think it looks so wrong in Kotlin. Narrow scope is important, but there are some subjective exceptions.
Functions serve more purpose than to reduce duplication. They also improve readability by breaking functions into smaller pieces (also important for unit testing) and reducing deeply nested/indented code. Sometimes that's worth expanding the scope of the function into the class.

However, I do sometimes use the cousin of the nested function, the lambda, which I find more visually acceptable especially if it's small.
Just the first 2 lines would change, like so:

val containsOrMatchesRegex = { it: String ->
	if (it.startsWith(Constants.REGEX_PREFIX))
	...

Usage doesn't change.

Anyway, I won't let this block the PR, but I'll eventually change it to match my code style preference if you leave it.

Also, the toast should be only when saving the regex no? There is not reason to put it on the service

Ideally, yes, as I previously suggested.
This is the lazy option which I will also accept. I just want something that could make the user aware that the regex is bad. I would likely move the validation to on-save after the merge if you don't.

Sorry, but I didn't understand what I forgot, could you please refer me to the file?

My mistake on the text replace regex. I saw the (?i) and didn't pay attention to it being for the plain text matching. The ignore/require regex still needs fixed (see other comment on the line).

@@ -167,7 +167,7 @@
<string name="tts_message_dialog">#A = App title\n#T = Ticker\n#S = Subtext\n#C = Content title\n#M = Content message\n#I = Content info\n#H = Big content title\n#Y = Big content summary\n#B = Big content text\n#L = Text lines\nCase insensitive\n\nDefault:\n%1$s\n\nOld default (v1.1.0 - v1.3.0):\n#A. #C. #M.\n\nOld default (v1.0.x):\n#A: #T</string>
<string name="tts_text_replace">TTS Text Replacement</string>
<string name="tts_text_replace_summary">Substitute text to be spoken, such as to fix pronunciation</string>
<string name="tts_text_replace_dialog">Substitute text to be spoken, allowing you to customize how Text-To-Speech pronounces words or replace text for any other reason.\n\nText to replace is case insensitive and applies after TTS Message formatting and before emoji removal.</string>
<string name="tts_text_replace_dialog">Substitute text to be spoken, allowing you to customize how Text-To-Speech pronounces words or replace text for any other reason.\n\nText to replace is case insensitive and applies after TTS Message formatting and before emoji removal.\nTo use regex, type the prefix $regex: and then your regex.\nFor example: $regex:[^xyz].\nRegex is case sensitive.</string>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a blank line (\n\n) before the regex part in both strings would look better to separate it from the basic usage instructions.

@pilot51
Copy link
Owner

pilot51 commented Dec 27, 2024

I know you've been busy, but I plan to merge this weekend and make the changes if you don't.

The important one is removing RegexOption.IGNORE_CASE at Service:254.
Validation on save would also be great but not critical.

I plan to add some useful examples to the string after merge to satisfy some of the requests I've been getting such as filtering out URLs. I would add the blank line when I do that, so don't worry about it. In fact, I may make the regex paragraph a separate string and append it to the rest of the description to eliminate the duplication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Regex support for Ignore/Require Text Regular expression for TTS Text Replacement
2 participants