Otra posible solución al problema del post anterior consiste en recorrer varias veces la misma línea del documento Markdown, sustituyendo un link cada vez, hasta encontrarlos todos.

La ventaja principal de esta solución es que se puede implementar totalmente dentro del stream, sin salirnos del contexto de la IO:

final class MarkdownTransformationUseCase:
  // -8<-- código omitido para abreviar
  private[this] def transform(line: Line) =
    Stream
      .unfoldChunkEval[IO, Option[String], Line](Some(line.value)) {
        case Some(text) =>
          (text match
            case linkPattern(text, url) => Some(Link(LinkText(text), LinkUrl(url)))
            case _ => Option.empty
          ).fold(IO.delay(Some((Chunk(Line(text)), Option.empty)))) { link =>
            for
              reference <- footnotesRepository.save(link)
              transformedText = linkPattern.replaceFirstIn(
                text,
                s"${link.text} [^${reference.value}]",
              )
            yield Some((Chunk(Line(transformedText)), Some(transformedText)))
          }
        case None => IO.none
      }
      .lastOr(Line.empty)

Aunque el coste computacional de esta solución es mayor que el de la anterior, en el mejor de los casos, la solución sigue teniendo una complejidad lineal.

Expresiones Regulares#

Aunque en toda la kata usé solamente una expresión regular, hice bastante para conseguir que fuera lo más expresiva posible:

object MarkdownTransformationUseCase:
  private val linkPattern = 
    raw"(?<!!)\[(?<text>[^\^].+?)]\((?<url>.+?)\)".r.unanchored

El negative look around al principio de la expresión asegura que no haga match con una imagen. En Markdown los links y las imágenes comparten un formato similar. Por ejemplo:

![This is an image](/assets/images/this-is-an-image.jpg "This is an image")

[And this is a link](http://example.org)

Otro detalle de la expresión regular es que utiliza named capturing groups para extraer los grupos. En Scala es muy cómodo reutilizar los mismos nombres dentro del pattern-matching:

case linkPattern(text, url) => (LinkText(text), LinkUrl(url))

Finalmente#

Lo que parecía un problema simple terminó dando mucho de sí y me mantuvo pensando durante días en nuevas formas de mejorar mi solución. Lo he disfrutado mucho y agradezco a Lean Mind que hayan compartido la kata.

Scalameta