I've made a custom [pause] tag which partly works - basically if it finds the tag, it adds the absolute index of the character to an array, and then in my typewriter script if the visible character count is equal to the first value in the array, increase the wait delay and remove the first entry in the pause array so it can move onto the next.

extends RichTextEffect
class_name RTEPause

var bbcode := "pause"
var done = false

func _process_custom_fx(char_fx):
	if not done:
		done = true
		Global.text_pause_positions.push_back(char_fx.absolute_index)
	return true

This works for the first character, but if I include multiple [pause] tags only the first index gets added.

I feel like it's due to the done flag that makes the function only run once, maybe it doesn't get set back to false so the next tag doesn't actually do anything?

So _process_custom_fx() gets called once per character. You might be able to just remove the if statement and it might work, but I haven't worked with rich text effects yet so I'm not sure.

func _process_custom_fx(char_fx):
    Global.text_pause_positions.push_back(char_fx.absolute_index)
    return true

nvm, solved it by tinkering around with substrings. :) Ignoring the bbcode tags was kinda tricky and using a boolean for counting seems kinda weird, but it works fine I guess.

var counting = false
    	
dirty_text = testmsg
    			
	# 'strip' the bbcode by finding a [, and then counting how many characters there are until a ] is found.
	for i in dirty_text.length():
    		
	var sub = dirty_text.substr(i, 1)
    		
	if sub == "[":
		counting = true
	if sub == "]":
	# this character should be counted too
		bbcode_count += 1
		counting = false
	if counting:
		bbcode_count += 1
    			
    	
	for i in dirty_text.length():
    		
		var sub = dirty_text.substr(i, 1)
    		
		if sub == "^":
			dirty_text.erase(i, 1)
			# add this position into the array
			pause_positions.push_back(i - bbcode_count)
			# get the timing
			var mod = (dirty_text.substr(i, 3))
			pause_timings.push_back(float(mod))
			dirty_text.erase(i, 3)
    			
		# update the text
		label.bbcode_text = dirty_text
		clean_text = label.text
2 years later