Terminada la corrección de la Tarea 2, que nuevamente podía resolverse
mirando el código fuente de Control.Monad.Identity, resta aclarar que la
identidad propuesta es cierta. A continuación una posible respuesta para
la Pregunta 2.
> import Control.Monad
> import Text.ParserCombinators.Parsec
> import System
Un archivo .lhs tiene cero o más líneas de contenido, seguidas por el
fin de archivo. En ese caso el reconocedor produce la lista de líneas
reconocidas.
> litfile = do
> html <- many content
> eof
> return html
Una línea de contenido puede ser un bloque de código, una línea de
encabezado o un párrafo. Noten que los prefijos izquierdos de codeBlock
("> ") y headerLine ("*" o "#") son disjuntos, así que no necesito usar
try.
> content = codeBlock
> <|> headerLine
> <|> paragraph
Un bloque de código tiene múltiples líneas de código continuas, y el
reconocedor debe envolverlas en un bloque HTML , insertando
saltos de línea entre ellas.
> codeBlock = do
> code <- many1 codeLine
> return $ "
\n" ++ unlines code ++ "
\n"
Una línea de código comienza por "> ", que ignoramos, y luego se
extiende hasta el final de la línea para conservar espacios en blanco y
cualquier caracter presente conviertiendo a entidades, excepto el final
de línea.
> codeLine = do
> string "> "
> content <- many entity
> char '\n'
> return $ concat content
En todas las respuestas noté que (por falta de costumbre o por apuro) no
factorizan código [1] sino que hacen "copy&paste" y cambian cosas
puntuales. El típico caso era tener dos combinadores para reconocer los
encabezados, y el combinador para los párrafos que son muy similares y
lo único que cambia es el envoltorio. Al no factorizar, el mantenimiento
se complica pues hay múltiples rutinas similares que deben estar en
sincronía.
Entonces, escribí una función auxiliar para "envolver" un contenido en
marcas HTML concordantes, parametrizable y extensible con facilidad...
> wrap t s = "<" ++ tag ++ ">" ++ s ++ "" ++ tag ++ ">\n"
> where tag = case t of
> '*' -> "h1"
> '#' -> "h2"
> 'p' -> "p"
...y entonces puedo tener _un_ sólo combinador para procesar la línea de
encabezados. La línea comienza por alguno de los símbolos válidos para
el encabezado, luego ignoro cualquier cantidad de espacios que pueda
haber y reconozco el resto de los caracteres hasta el final de la línea,
consumiendo este último. El reconocedor envuelve la línea con la marca
adecuada antes de retornarla.
> headerLine = do
> spaces
> t <- oneOf "*#"
> spaces
> content <- many entity
> char '\n'
> return $ wrap t $ concat content
Un párrafo es un conjunto de múltiples líneas que deben tener una línea
en blanco inmediatamente después. Noten como, nuevamente, aprovecho mi
función auxiliar para envolver el contenido con la marca adecuada.
> paragraph = do
> content <- many1 line
> spaces
> return $ wrap 'p' $ "\n" ++ unlines content
Una línea de un párrafo debe tener al menos una entidad y un final de
línea, que se consume.
> line = do
> content <- many1 cleanEntity
> char '\n'
> return $ concat content
Una entidad es cualquier caracter, posiblemente convertido a su forma
escape HTML. Una entidad "limpia" es aquella que colapsa varios espacios
en blanco consecutivos por uno sólo.
> cleanEntity = (many1 (oneOf " \t") >> return " ")
> <|> entity
>
> entity = (char '>' >> return ">")
> <|> (char '<' >> return "<")
> <|> (char '&' >> return "&")
> <|> (noneOf "\n\r" >>= \c -> return [c])
> > " a character"
El encabezado y pié de página mínimos para un documento HTML válido. La
mayoría olvidó este detalle.
> header t = "