Итак, вот один из способов.
Начнем с определения местоположения всех тегов в необработанном тексте.
val text =
"Lorem ipsum dolor <a>Hello <b>Nested</b> World</a> sit amet, consectetur"
val tags = "<([^>]+)>".r.findAllMatchIn(text)
.map(m => (m.start,m.end-m.start,m.group(1)))
.toList
//tags: List[(Int, Int, String)] = List((18,3,a), (27,3,b), (36,4,/b), (46,4,/a))
Теперь, когда у нас есть все местоположения и длины теговпо порядку мы можем очистить текст.
val cleanTxt = tags.foldLeft((text,0)){
case ((str,acc),(x,len,_)) => (str.patch(x-acc,"",len),acc+len)
}._1
//cleanTxt: String = Lorem ipsum dolor Hello Nested World sit amet, consectetur
Немного сложнее связать места начала / конца тега с учетом их соответствующих положений в строке чистого текста.
val cleanTags = tags.foldLeft((List.empty[(Int,String)],0)){
case ((lst,acc),(x,l,n))=> ((x-acc,n)::lst,acc+l)
}._1.reverse
//cleanTags: List[(Int, String)] = List((18,a), (24,b), (30,/b), (36,/a))
def pairTags(ts :Vector[(Int,String)]
,acc :List[(Int,Int,String)] = List()
) :List[(Int,Int,String)] =
if (ts.isEmpty) acc.reverse
else {
val hd = ts.head
val matchX = ts.lastIndexWhere(_._2 == "/" + hd._2)
assert(matchX > 0, "bad tag collection")
pairTags(ts.tail.patch(matchX-1,Vector(),1)
,(hd._1,ts(matchX)._1,hd._2) :: acc)
}
pairTags(cleanTags.toVector)
//res0: List[(Int, Int, String)] = List((18,36,a), (24,30,b))
Обратите внимание, что это местоположения индексов «от» (включительно) и «до» (исключая) для каждого диапазона текста.