Saltar al contenido

Un editor de párrafos escrito en C++

Introducción.

He tenido que realizar esta práctica para la asignatura Tecnología de Objetos de la carrera. Dicha práctica consiste en implementar un editor de párrafos con estilos y posibilidad de exportarlos a lenguaje de marcado HTML.
El objetivo es tener una serie de clases contenedoras (párrafos y líneas) que contengan las palabras, haciendo distinción de si una palabra es la primera de un párrafo, la cual se visualizará por pantalla con una tabulación a su izquierda.
El menú presentado al usuario debe permitir realizar las siguiente operaciones:
  • Borrar un párrafo seleccionado por el usuario.
  • Insertar un párrafo (se permite hacerlo entre dos párrafos ya existentes).
  • Volcar un párrafo a lenguaje HTML.
Los estilos a la hora de volcar una palabra pueden ser negrita, cursiva y subrayado. Otras consideraciones a tener en cuenta son:
  • La existencia de una clase base de la que derivan palabra, línea y párrafo dónde se definirá la forma de escribirse en pantalla y su forma de volcarse a HTML.
  • A la hora de hacer el vuelco, el programa escribirá y en el medio pedirá a cada párrafo que vuelque su contenido a HTML.
  • El número de letras de las palabras en cada línea no puede ser igual o superior a 80.

Implementación.

Para implementar el editor de párrafos, se hizo incapié en conseguir el diseño más adecuado para modelar el problema. Al haber un editor, varios estilos, una interfaz y contenedores, se intentó orquestar todos los componentes de la mejor forma posible, facilitando el mantenimiento futuro así como modificaciones y posibilitar cómodamente la inclusión de nuevas funcionalidades o elementos. Esto último es: que sea fácilmente extensible la parte de los contenedores, los estilos y las etiquetas HTML. Por ejemplo, se podría convertir el programa en un editor de textos, en el cual no hay sólo párrafos, sino también contenedores superiores como páginas y secciones. También pueden surgir necesidades tales como soportar la inclusión de listas y su conversión a HTML.
Para conseguir todo esto se utilizó un patrón de diseño específico, el denominado patrón fachada o facade que permite separar el modelado del editor en sí mismo de la interfaz proporcionando una serie de operaciones que, en casao de modificarlas siguen siendo transparentes al resto de elementos que interactúen con el editor en sí mismo de la interfaz proporcionando una serie de operaciones que, en caso de modificarlas siguen siendo transparentes al resto de elementos que interactúen con el editor y que permita, por ejemplo, cambiar la interfaz de consola por otra de tipo gráfico de forma modular.
Como los párrafos y su contenido se almacenan en memoria, desconociendo de antemano cuántos elementos y tamañao se va a ocupar, se almacentan en el heap o zona de memoria dinámica por motivos de flexibilidad y todos ellos se destruyen liberando la memoria que ocupan antes de finalizar el programa.
Durante la implementación de la práctica no todo fue sobre ruedas. Primero, el código generado por ArgoUML fue bastante incompleto y con líneas innecesarias por lo que hubo prácticamente que modificarlo por completo.
Después hubo algún que otro error de violación de segmento ante la falta de práctica de trabajar con memoria dinámica y gestionarla correctamente. También los errores comunes de sintaxis o de omisión que provocaban un incorrecto funcionamiento. Para solucionarlo, se fueron acotando los posibles errores mediante mensajes de depuración hasta dar con el fallo. Estos mensajes muestran los estados de los contenedores, valores de las variables etc… Obviamente todos estos mensajes fueron eliminados del código fuente y, esta forma de depurar un tanto arcaica a veces compensa antes que un debugger en función de la naturaleza del error.
Finalmente se ha comentado el código y estructurado de la mejor manera posible para facilitar su rápida lectura y comprensión. Al compilar no existen warnings.

Uso del editor de párrafos.

La utilización del editor de párrafoes es muy sencilla. Se inicia el ejecutable y nos encontramos por consola con el siguiente menú:
Ilustración 1: Menú del editor de párrafos.

Crear un nuevo párrafo.

A continuación podemos introducir un párrafo escribiendo 1 como opción y pulsando enter. Como es el primer párrafo que introducimos, se nos mostrará directamente una pantalla para poder escribir el párrafo. Cuando escribamos un salto de línea el editor asumirá este como fin del párrafo. Nótese que el editor limpia la pantalla.
Ilustración 2: Creación del primer párrafo.
El párrafo quedará guardado en memoria dinámica. Si hay más de dos párrafos, se nos permite elegir si deseamos almacenar el párrafo entre otros dos ya existentes. En este ejemplo hay dos párrafos, se nos muestran por pantalla y se pide introducir la posición: Si se introduce una no válida se pregunta de nuevo y, en caso de seleccionar la posición 2, el nuevo párrafo ocuparía esa posición y el párrafo segundo se vería desplazado a la posición 3:
Ilustración 3: Crear un nuevo párrafo entre dos ya existentes.
Los párrafos quedarían de la siguiente forma. El último párrafo introducido tiene un salto de línea, ya que su primera línea posee una cantidad de caracteres en sus palabras superior o igual a 80.
Ilustración 4: Listado de párrafos tras introducir uno en medio de otros dos.
Pero… ¿Qué hacer para indicar que una palabra tiene un estilo concreto? Pues muy fácil; basta con marcarla al principio con:
  • @n para obtener una palabra en negrita.
  • @c para identificar una palabra en cursiva.
  • @s para marcar la palabra como subrayada.
Entre la marca y la palabra no pueden existir espacios. La elección de esta marca se debe a que no existen palabras que comiencen por el carácter arroba y a que no tiene sentido marcarlas con HTML ya que ese es el lenguaje al que se volcarán los contenidos.
Ilustración 5: Marcando las palabras con diferentes estilos.
Es importante saber que, si se entra en la opción de añadir un nuevo párrafo y queremos volver al menú principal porque no nos interesa guardarlo podemos hacerlo seleccionando cero (0) como posición del nuevo párrafo o bien escribiendo un párrafo vacío, el cual se desechará por no contener palabras ni espacios en blanco.

Eliminar un párrafo.

Para eliminar un párrafo, basta con seleccionar la opción 2 del menú del editor, mostrándose los párrafos existentes para seleccionar el que el usuario desee borrar. Un párrafo eliminado no se vuelve a recuperar ya que es eliminado de la memoria.
Ilustración 6: Listado de párrafos que se pueden eliminar.
Tras introducir el índice de un párrafo válido, éste se borra y el resto de párrafos con índices mayores se reestructuran ocupando posiciones anteriores. Si se entró en la opción de menú de borrar un párrafo por accidente, escribiendo cero (0) como posición del párrafo a eliminar el programa regresa al menú principal.

Volcar párrafo a HTML.

Para volcar un párrafo a HTML seleccionamos la opción 3 del menú del editor.
Ilustración 7: Listado de párrafos en memoria que pueden ser volcados a HTML.
Se selecciona un párrafo después de que se presenten por pantalla los que hay almacenados en memoria. Para este ejemplo se selecciona el 3:
Ilustración 8: Ejemplo de volcado de párrafo a HTML.
Como se puede observar, el programa vuelca todo el párrafo a HTML, colocando las etiquetas de inicio y fin de párrafo al principio y al final del párrafo, indentando su contenido para facilitar la lectura. El fin de cada línea se marca con un salto de línea y las palabras con estilo se rodean también de las etiquetas que corresponda. En caso de introducir una posición incorrecta de párrafo a volcar, se mostrará un error por pantalla al usuario y se regresará al menú principal.
Ilustración 9: Error de párrafo incorrecot a volcar.
Para finalizar, para salir del programa correctamente se introduce cero (0) como opción del menú principal, procedimiento el cual limpiará la memoria dinámica. En caso de cualquier otra opción no contemplada en el menú se mostrará un mensaje de error al usuario y se volverá a solicitar la opción.
Ilustración 10: Saliendo de la aplicación.

Diagrama de clases UML.

A grandes rasgos, el EditorDeParrafos es la clase fachada que proporciona una interfaz bien definida a la InterfazPorConsola, con las funcionalidades propias del editor siendo transparente su funcionamiento interno de cara al exterior. A su vez, se encarga de crear los contenedores y trabajar con ellos manteniéndolos en memoria, tratándolos como elementos de texto.

Ilustración 11: Diagrama de clases UML del editor de  párrafos.

Diagramas de secuencia UML.

A continuación se muestran los diagramas de secuencia generales de los tres casos de uso principales de la aplicación: Crear un párrafo, borrarlo y volcarlo a HTML.
Ilustración 12: Diagrama de secuencia: creación de un párrafo nuevo.
Ilustración 13: Diagrama de secuencia: borrar un párrafo.
Ilustración 14: Diagrama de secuencia de volcado de párrafo a HTML.

Código fuente

EditorDeParrafos.h

#ifndef EditorDeParrafos_h
#define EditorDeParrafos_h

#include <vector>
#include <string>
#include "ElementoDeTexto.h"

class EditorDeParrafos {

    public:

        EditorDeParrafos() { }

        std::string listarParrafos();

        void crearParrafo( std::string &texto, unsigned int posicion = 0 );

        bool borrarParrafo( unsigned int opcion );

        std::string volcarParrafoAHtml( unsigned int opcion );

        unsigned int getNumeroDeParrafos()
            { return elementos.size(); }

        ~EditorDeParrafos()
        {
            for ( int i = 0; i < elementos.size(); i++ )
                delete elementos[ i ];
        }

    private:

        std::vector<elementodetexto> elementos;

        std::string detectarEstilo( std::string texto );

        std::string eliminarMarcaDeEstilo( std::string texto );

};

#endif

EditorDeParrafos.cpp

#include <sstream>
#include <vector>
#include "Palabra.h"
#include "Linea.h"
#include "Parrafo.h"
#include "ElementoDeTexto.h"
#include "EditorDeParrafos.h"
#include "PrimeraPalabra.h"

/*  Retorna una cadena de caracteres con los párrafos
    almacenados en memoria del editor así como sus
    posiciones (orden). Si no se encuentran párrafos,
    retorna "No hay párrafos guardados que mostrar."   */
std::string EditorDeParrafos::listarParrafos()
{
    if ( elementos.empty() ) {
        return "No hay párrafos guardados que mostrar.";
    } else {
        for( unsigned int i = 0; i < elementos.size(); i++ ) {
            std::cout << "Párrafo " << i + 1 << ":" << std::endl;
            elementos[ i ]->escribirPorPantalla();
            std::cout << std::endl;
        }

        return "Fin listado párrafos encontrados.";
    }
}

/*  Función miembro que dado un párrafo en forma de texto y una posición,
    analiza todas sus palabras creando un objeto de la clase Parrafo en el lugar
    que corresponda, con las líneas (instancias de Linea) que a su vez contienen
    las palabras (objetos Palabra). La posición que recibe es la del usuario,
    es decir: de 1..N, mientras que los índices internos son de 0..N    */
void EditorDeParrafos::crearParrafo( std::string &texto, unsigned int posicion )
{
    std::vector<palabra> vectorDePalabras;
    std::vector<linea> vectorDeLineas;
    std::string estiloDetectado;
    Palabra *p = NULL;
    unsigned int contadorLongitud = 0; // Falta crear lineas nuevas si llega al tope.
    bool unicaPalabra = true;

    while ( texto.length() > 0 ) {
        // Si texto tiene longitud es que hay una o más palabras.
        if ( texto.find_first_of( " " ) != std::string::npos ) {
            // Si hay espacios en blanco significa que hay al menos dos palabras.
            unicaPalabra = false;
            if ( texto[ 0 ] == ' ' ) {
                // Si al principio del texto tenemos un espacio en blanco se elimina:
                texto = texto.substr( 1, texto.length() - 1 );
            } else {
                // Se detecta una nueva palabra y su estilo:
                estiloDetectado = detectarEstilo( texto.substr( 0, texto.find_first_of( " " ) ) );

                // Se elimina la marca de edición (@s, @n o @c):
                texto = eliminarMarcaDeEstilo( texto );

                // Si se va a leer la primera palabra:
                if ( vectorDePalabras.size() == 0)
                    p = new PrimeraPalabra( texto.substr( 0, texto.find_first_of( " " ) ), estiloDetectado );
                else // Si es otra:
                    p = new Palabra( texto.substr( 0, texto.find_first_of( " " ) ), estiloDetectado );

                vectorDePalabras.push_back( p );
                // Lo que queda por analizar:
                texto = texto.substr( texto.substr( 0, texto.find_first_of( " " ) ).length(), texto.length() - 1 );
            }
        } else {
            // Consumimos la última palabra comprobando su estilo:
            estiloDetectado = detectarEstilo( texto );

            // Se elimina la marca de edición (@s, @n o @c):
            texto = eliminarMarcaDeEstilo( texto );

            /*  Se comprueba si la palabra es la última o se trata de
                un párrafo con una única palabra.   */
            Palabra *p;
            if ( unicaPalabra )
                p = new PrimeraPalabra( texto , estiloDetectado );
            else
                p = new Palabra( texto , estiloDetectado );
            vectorDePalabras.push_back( p );

            // Ya no quedan palabras que buscar:
            texto = "";
        }
    }


    /*  Se recorre el vector de palabras creando el
        número de líneas que sea necesario. */
    unsigned int contadorDeCaracteres = 0;
    unsigned int ultimaPalabra = 0;
    std::vector <palabra> subvectorDePalabras;

    // Para cada palabra introducida por el usuario...
    for ( int i = 0; i < vectorDePalabras.size(); i++ ){

        /*  Si la longitud de la palabra actual junto con la de las anteriores
            no supera el máximo de caracteres permitido por línea, la Palabra
            se añade a la línea actual. */
        if ( contadorDeCaracteres + vectorDePalabras[ i ]->longitud() < 79 ) {
            subvectorDePalabras.push_back( vectorDePalabras[ i ] );
            contadorDeCaracteres += vectorDePalabras[ i ]->longitud();
            // Se comprueba si la palabra analizada es la última:
            if ( i == vectorDePalabras.size() - 1 ) {
                vectorDeLineas.push_back( new Linea (subvectorDePalabras) );
            }
        }

        /*  Si la longitud acumulada más la de la palabra actual supera el
            máximo permitido por línea, se crea una con las palabras acumuladas
            y la palabra actual formará parte de la línea siguiente.    */
        if ( contadorDeCaracteres + vectorDePalabras[ i ]->longitud() > 79 ) {
            vectorDeLineas.push_back( new Linea ( subvectorDePalabras ) );
            contadorDeCaracteres = 0;
            subvectorDePalabras.clear();
        }

        /*  Si la longitud acumulada más la de la palabra actual iguala el
            máximo permitido por línea, se crea una con las palabras acumuladas
            junto con la palabra actual.    */
        if ( contadorDeCaracteres + vectorDePalabras[ i ]->longitud() == 79 ) {
            vectorDeLineas.push_back( new Linea ( subvectorDePalabras ) );
            contadorDeCaracteres = 0;
            subvectorDePalabras.clear();
        }
    }

    /*  Creamos el párrafo y lo almacenamos con el resto en  la
        posición que corresponda:   */
    ElementoDeTexto *nuevoParrafo = new Parrafo( vectorDeLineas );

    if ( posicion == getNumeroDeParrafos() + 1 || getNumeroDeParrafos() == 0 ) {
        this->elementos.push_back( nuevoParrafo );
    } else {
        posicion--;
        std::vector<elementodetexto> vectorAuxiliar;

        for ( int i = getNumeroDeParrafos() - 1; i >= posicion; i-- ) {
            vectorAuxiliar.push_back( elementos[ i ] );
            this->elementos.pop_back();
        }

        this->elementos.push_back( nuevoParrafo );

        for ( int j = vectorAuxiliar.size() - 1; j >= 0; j-- ) {
            elementos.push_back( vectorAuxiliar[ j ] );
            vectorAuxiliar.pop_back();
        }
    }
}

bool EditorDeParrafos::borrarParrafo( unsigned int opcion )
{
    if ( ( opcion >= 0 ) && ( opcion < elementos.size() ) ) {
        std::vector<elementodetexto> aux;

        // Se localiza el elemento a borrar.
        for ( int i = elementos.size() - 1; i != opcion; i-- ) {
            aux.push_back( elementos[ i ] );
            elementos.pop_back();
        }

        // Se borra el párrafo seleccionado de la memoria dinámica.
        delete elementos[ elementos.size() - 1 ];
        // Se elimina su puntero del vector elementos.
        elementos.pop_back();

        // Se restauran el resto de párrafos manteniendo el orden.
        for ( int i = aux.size() - 1; i >= 0; i-- ) {
            elementos.push_back( aux [ i ] );
            aux.pop_back();
        }

        return true;
    } else {
        return false;
    }
}

/*  A partir de una posición del vector de párrafos, obtiene el
    párrafo que la ocupa y lo convierte a HTML. Internamente
    trabaja siendo 0 la primera posición.   */
std::string EditorDeParrafos::volcarParrafoAHtml( unsigned int opcion )
{
    opcion--;
    if ( ( opcion >= 0 ) && ( opcion < elementos.size() ) )
        return elementos[ opcion ]->volcarAHtml();
    else
        return "ERROR: El párrafo introducido no se puede volcar.";
}

/*  Función miembro que detecta el estilo de un texto (si lo tiene) */
std::string EditorDeParrafos::detectarEstilo( std::string texto )
{
    // Por defecto se carece de estilo:
    std::string toret = "";

    if ( texto.length() >= 3 ) {

        // La palabra tiene estilo negrita.
        if ( texto.substr(0, 2).compare("@n") == 0 )
            toret = "b";

        // La palabra está subrayada:
        if ( texto.substr(0, 2).compare("@s") == 0 )
            toret = "u";

        // La palabra es cursiva.
        if ( texto.substr(0, 2).compare("@c") == 0 )
            toret = "i";
    }

    return toret;
}

/*  Función miembro que elimina la marca de estilo introducida por
    el usuario a un texto. Si no tiene marca, se devuelve el texto
    sin modificación alguna.    */
std::string EditorDeParrafos::eliminarMarcaDeEstilo( std::string texto )
{
    std::string estiloDetectado = detectarEstilo( texto );
    if ( estiloDetectado.compare( "" ) == 0 )
        return texto;

    if ( estiloDetectado.compare( "b" ) == 0
            || estiloDetectado.compare( "u" ) == 0
                || estiloDetectado.compare( "i" ) == 0 )
        return texto.substr( 2, texto.length() );
}

ElementoDeTexto.h

#ifndef ElementoDeTexto_h
#define ElementoDeTexto_h

#include <string>
#include "EtiquetaHtml.h"

class ElementoDeTexto {

    public:

        ElementoDeTexto( const std::string &e, const bool &b = false ) : etiquetaHtml( e, b ) { }

        virtual void escribirPorPantalla() = 0;

        virtual std::string volcarAHtml() = 0;

        std::string getEtiquetaApertura()
            { return etiquetaHtml.getEtiquetaApertura(); }

        std::string getEtiquetaCierre()
            { return etiquetaHtml.getEtiquetaCierre(); }

        ~ElementoDeTexto() { }

    private:

        EtiquetaHtml etiquetaHtml;

};

#endif

EtiquetaHtml.h

#ifndef EtiquetaHtml_h
#define EtiquetaHtml_h

#include <string>

class EtiquetaHtml {

    public:

        EtiquetaHtml( const std::string &c = "", const bool &s = false ): contenido( c ), simple( s ) { }

        std::string getEtiquetaApertura();

        std::string getEtiquetaCierre();

        ~EtiquetaHtml() { }

    private:

        std::string contenido;

        // Una etiqueta puede ser simple como 
, o doble, como .
        bool simple;

};

#endif

EtiquetaHtml.cpp

#include "EtiquetaHtml.h"
#include <sstream>
#include <string>
#include <iostream>

 std::string EtiquetaHtml::getEtiquetaApertura()
 {
    if( simple || !contenido.compare("") ) {// Una etiqueta simple sólo tiene elemento de cierre.
        return "";
    } else {
        std::ostringstream toret;
        toret << "<" << contenido << ">";
        return toret.str();
    }
 }

 std::string EtiquetaHtml::getEtiquetaCierre()
 {
    std::ostringstream toret;

    if ( !contenido.compare("") )
        return "";

    if( simple )// Una etiqueta doble cierra con </...>.
        toret << "<" << contenido << "/>";
    else
        toret << "</" << contenido << ">";

    return toret.str();
 }

InterfazPorConsola.h

#ifndef InterfazPorConsola_h
#define InterfazPorConsola_h

#include "EditorDeParrafos.h"

class InterfazPorConsola {

    public:

        InterfazPorConsola() { }

        void  iniciarInteraccion();

        ~InterfazPorConsola() { }

    private:

        EditorDeParrafos editor;

        void mostrarMenu();

        unsigned int introducirOpcion();

        bool esOpcionValida( unsigned int opcion );

        void procesarOpcion( unsigned int opcion );

        void limpiarPantalla();

};

#endif

InterfazPorConsola.cpp

/* Interfaz de usuario del programa mediante consola de comandos */
#include <iostream>
#include <string>
#include <cstdlib>
#include "InterfazPorConsola.h"
#include "EditorDeParrafos.h"

/* Inicia la interacción con el usuario */
void  InterfazPorConsola::iniciarInteraccion()
{
    unsigned int opcionSeleccionada;

    do {
        mostrarMenu();
        opcionSeleccionada = introducirOpcion();
    } while ( opcionSeleccionada ); // Si la opción es 0 (falso) finaliza el bucle.
}

/* Método que muestra las distintas opciones del menú por la salida estándar */
void InterfazPorConsola::mostrarMenu()
{
    limpiarPantalla();
    std::cout << "----------------------------------------------"
        << "ntEDITOR DE PÁRRAFOS"
        << "ntFélix Mera García"
        << "ntfmgarcia@correo.ei.uvigo.es"
        << "n----------------------------------------------"
        << "nt1. Crear un nuevo párrafo."
        << "nt2. Borrar un párrafo existente."
        << "nt3. Volcar a HTML un párrafo existente."
        << "nnt0. Salir."
        << std::endl;
}

/*  Valida una opción, retornando TRUE si es correcta y FALSE
    si no lo es.    */
bool InterfazPorConsola::esOpcionValida( unsigned int opcion )
{
    if ( opcion >= 0 && opcion <= 3 )
        return true;
    else
        return false;
}

/*  Función miembro de InterfazPorConsola que solicita al usuario
    una opción por teclado. Si esta es incorrecta se le solicita
    de nuevo hasta que sea válida.  */
unsigned int InterfazPorConsola::introducirOpcion()
{
    unsigned int opcion;
    bool opcionValida = false;

    do {
        std::cout << "nOpción: ";
        std::cin >> opcion;
        opcionValida = esOpcionValida( opcion );

        if ( !opcionValida ) {
            std::cout << "nERROR: La opción que ha introducido es incorrecta." << std::endl;
        }
    } while ( !opcionValida );

    procesarOpcion( opcion );

    return opcion;
}

/*  Recibe como parámetro el número de una opción presentada
    en el menú de usuario y ejecuta las acciones que correspondan
    a la misma. */
void InterfazPorConsola::procesarOpcion( unsigned int opcion )
{
    unsigned int posicionParrafo;
    std::string nuevoTexto;

    switch( opcion ) {

        case 0: // Opción salir del programa.
            limpiarPantalla();
            std::cout << "Limpiando memoria..." << std::endl;

            // Liberamos la memoria dinámica ocupada:
            if ( editor.getNumeroDeParrafos() == 0 ) {
                for ( int i = 0; i < editor.getNumeroDeParrafos(); i++ )
                    if ( editor.borrarParrafo( i ) )
                        std::cout << "Párrafo " << i << " eliminado de memoria dinámica." << std::endl;
            }
            std::cout << "Saliendo..." << std::endl;
            break;

        case 1: // Opción para crear un nuevo párrafo.
            limpiarPantalla();
            std::cout << "Listando párrafos...n" << std::endl;
            std::cout << editor.listarParrafos() << std::endl;

            /*  Si hay más de un párrafo, el nuevo se inserta
                en la posición que desee el usuario:  */

            if ( editor.getNumeroDeParrafos() > 0 ) {

                do {
                    if ( editor.getNumeroDeParrafos() >= 2 ) {
                        std::cout << "nInserte la posición del nuevo párrafo (2-"
                            << editor.getNumeroDeParrafos() + 1 << "): ";
                        std::cin.ignore();
                        std::cin >> opcion;
                    } else {
                        opcion = editor.getNumeroDeParrafos() + 1;
                    }
                } while ( opcion < 0 || opcion > editor.getNumeroDeParrafos() + 1 );

            }

            limpiarPantalla();

            if ( opcion != 0 ) {
                std::cout << "Redacte el nuevo párrafo:n" << std::endl;
                std::cin.ignore();
                std::getline( std::cin, nuevoTexto );

                if ( nuevoTexto.empty() ){
                    std::cout << "Párrafo vacío. Se omite la creación del nuevo párrafo."
                              << std::endl;
                } else {
                    if ( editor.getNumeroDeParrafos() == 0 )
                        editor.crearParrafo( nuevoTexto );
                    else
                        editor.crearParrafo( nuevoTexto, opcion );
                }
            }
            break;

        case 2: // Opción para borrar un párrafo.
            limpiarPantalla();
            std::cout << "Listando párrafos...n" << std::endl;
            std::cout << editor.listarParrafos() << std::endl;

            if ( editor.getNumeroDeParrafos() != 0 ) {
                std::cout << "Seleccione el párrafo a eliminar:" << std::endl;
                std::cin >> posicionParrafo;
                if ( editor.borrarParrafo(--posicionParrafo) )
                    std::cout << "Párrafo eliminado." << std::endl;
                else
                    std::cout << "ERROR al eliminar el párrafo seleccionado." << std::endl;
            }
            break;

        case 3: // Opción para volcar un párrafo a HTML.
            limpiarPantalla();
            std::cout << "Listando párrafos...n" << std::endl;
            std::cout << editor.listarParrafos() << std::endl;

            if ( editor.getNumeroDeParrafos() != 0 ) {
                do {
                    std::cin.ignore();
                    std::cout << "Seleccione el párrafo a volcar a HTML: ";
                    std::cin >> posicionParrafo;
                } while (std::cin.fail());// FALLA SI NO ES UN NUMERO
                std::cout << "Volcando el párrafo " << posicionParrafo << " a HTML...n"
                    << std::endl;
                std::cout << editor.volcarParrafoAHtml( posicionParrafo );
                std::cout << "nPulse una ENTER para volver al menú..." << std::endl;
                std::string s;
                std::cin.ignore();
                getline(std::cin, s, 'n');
            }
            break;
    };
}

void InterfazPorConsola::limpiarPantalla()
{
    #ifdef WINDOWS
        std::system( "CLS" );
    #else
        std::system( "clear" );
    #endif
}

// Programa principal.
int main()
{
    InterfazPorConsola *interfaz = new InterfazPorConsola();
    interfaz->iniciarInteraccion();
    delete interfaz;

    return 0;
}

Linea.h

#ifndef Linea_h
#define Linea_h

#include "Palabra.h"
#include <vector>
#include "ElementoDeTexto.h"
#include <string>

class Linea : public ElementoDeTexto {

    public:

        Linea( std::vector<palabra> palabras ) : ElementoDeTexto( "br", true )
            { this->palabras = palabras; }

        void escribirPorPantalla();

        std::string volcarAHtml();

        unsigned int getNumeroMaximoDeCaracteres()
            { return NUMERO_MAXIMO_DE_CARACTERES; }

        ~Linea()
        {
            for ( unsigned int i = 0; i < palabras.size(); i++ )
                delete palabras[ i ];
        }

    private:

        const static unsigned int NUMERO_MAXIMO_DE_CARACTERES = 79;

        std::vector<palabra> palabras;

};

#endif

Linea.cpp

#include "Linea.h"
#include "Palabra.h"
#include <vector>
#include <sstream>

/*  Método miembro del contenedor Linea que recorre el
    vector con todas las palabras, imprimiéndolas en la
    salida estándar e intercalando espacios en blanco
    entre las mismas.   */
void Linea::escribirPorPantalla()
{
    for (int i = 0; i < palabras.size(); i++) {
        palabras[ i ]->escribirPorPantalla();
        // Tras la última palabra de una línea no va
        // ningún espacio en blanco:
        if ( i != palabras.size() - 1 )
            std::cout << " ";
    }

    std::cout << std::endl;
}

std::string Linea::volcarAHtml()
{
    std::ostringstream toret;
    for (int i = 0; i < palabras.size(); i++) {
        toret << palabras[ i ]->volcarAHtml();
        // Tras la última palabra de una línea no va
        // ningún espacio en blanco:
        if ( i != palabras.size() - 1 )
            toret << " ";
    }

    toret << this->getEtiquetaCierre() << std::endl;
    return toret.str();
}

Palabra.h

#ifndef Palabra_h
#define Palabra_h

#include "ElementoDeTexto.h"
#include <iostream>
#include <string>
#include <sstream>
#include "EtiquetaHtml.h"

class Palabra : public ElementoDeTexto {

public:

        Palabra( const std::string &t, const std::string &e = "" ) : ElementoDeTexto( e ), texto( t ) { }

        std::string getTexto() const
            { return texto; }

        void escribirPorPantalla()
            { std::cout << texto; }

        std::string volcarAHtml()
        {
            std::ostringstream toret;
            toret << getEtiquetaApertura() << texto << getEtiquetaCierre();
            return toret.str();
        }

        unsigned int longitud()
            { return texto.length(); }

        ~Palabra() { }

    private:

        std::string texto;

};

#endif

Parrafo.h

#ifndef Parrafo_h
#define Parrafo_h

#include "ElementoDeTexto.h"
#include "Linea.h"
#include <vector>
#include <string>

class Parrafo : public ElementoDeTexto {

    public:

        Parrafo( const std::vector<linea> lineas, unsigned int ndo = 0) : ElementoDeTexto( "p" ), numDeOrden( ndo )
            { this->lineas = lineas; }

        void escribirPorPantalla();

        std::string volcarAHtml();

        unsigned int getNumDeOrden()
            { return numDeOrden; }

        ~Parrafo()
        {
            for ( unsigned int i = 0; i < lineas.size(); i++ )
                delete lineas[ i ];
        }

    private:

        unsigned int numDeOrden;

        std::vector<linea> lineas;

};

#endif

Parrafo.cpp

#include "Parrafo.h"
#include <iostream>
#include <sstream>

void Parrafo::escribirPorPantalla()
{
    for( unsigned int i = 0; i < lineas.size(); i++ ) {
        lineas[ i ]->escribirPorPantalla();
    }
}

std::string Parrafo::volcarAHtml()
{
    std::ostringstream toret;
    toret << ElementoDeTexto::getEtiquetaApertura() << std::endl;

    for( unsigned int i = 0; i < lineas.size(); i++ )
        toret << "t" << lineas[ i ]->volcarAHtml();

    toret << ElementoDeTexto::getEtiquetaCierre() << std::endl;
    return toret.str();
}


PrimeraPalabra.h

#ifndef PrimeraPalabra_h
#define PrimeraPalabra_h

#include "Palabra.h"

class PrimeraPalabra : public Palabra {

    public:

        PrimeraPalabra( const std::string &t, const std::string &e = "" ): Palabra( t, e ) {}

        void escribirPorPantalla();

        ~PrimeraPalabra() {}

};

#endif

PrimeraPalabra.cpp

#include "PrimeraPalabra.h"
#include <string>

void PrimeraPalabra::escribirPorPantalla()
{
    std::cout << "t";
    Palabra::escribirPorPantalla();
}

Posibles ampliaciones y mejoras.

Siempre hay aspectos a mejorar en una práctica, y más teniendo en cuenta de que no se dispone de todo el tiempo que se requiere porque como todo proyecto posee unos plazos establecidos para la entrega. Aún así, a continuación se enumeran una serie de ampliaciones funcionales y mejoras que se podrían implementar:
  • Utilización de librerías de texto para al listar párrafos, mostrar las palabras que corresponda con sus estilos (palabras subrayadas, en negrita, etc…). Todo lo que encontré no era estándar, y salvo utilizar librerías como ncurses desconozco si hay otras opciones.
  • Utilización de una interfaz gráfica para mejorar la experiencia de usuario al utilizar el programa, ya que será más intuitiva que la interacción por consola, aunque se hizo incapié en conseguir una interacción sencilla e intuitiva.
  • Utilización de Doxygen. Al ser un proyecto pequeño, los comentarios son los propios de C++. aunque en proyectos de mayor embergadura es conveniente utilizar comentarios como los de JAVADOC de JAVA, pues facilitan la generación de la documentación técnica.
  • Ampliación de contenedores y marcas HTML tal y como se comenta en la introducción: que existan páginas y secciones y otros elementos de formato HTML.
  • Refactorización del tratamiento de errores ya que el actual es muy simple. Convendría utilizar excepciones categorizándolas en diferentes tipos, lo cual hará la aplicación más segura en caso de extenderla.

Publicado enApuntesProgramación de alto nivel