Sesión 10: Representando HTML
En este laboratorio vamos a definir diferentes tipos en C para poder representar HTML usando struct y union.
HTML
HTML es el lenguaje de marcado con el que se crean las páginas
Web. Veamos un ejemplo de fichero HTML, un hello world (puedes
copiarlo, salvarlo en un fichero con extensión .html y abrirlo con un
navegador Web):
1
2
3
4
5
6
7
8
9
10
11
|
<html>
<head>
<meta charset="utf-8"></meta>
<title>Página Hello world</title>
</head>
<body>
<p style="text-align: center; background-color: #f0f8ff; color: #2c3e50; padding: 15px; border-radius: 8px; font-family: Arial, sans-serif; font-size: 16px;">
Hello world
</p>
</body>
</html>
|
Vamos a diseccionar lo que ves, de forma un tanto informal de momento:
- En un fichero HTML hay principalmente elementos y atributos.
- Los elementos son fácilmente distinguibles porque usan etiquetas
como por ejemplo
<html>
, <head>
, <meta>
, <title>
, <body>
,
y <p>
.
- Los atributos también se pueden distinguir fácilmente porque tiene
la forma
nombre_de_atributo="valor del atributo"
, como por ejemplo
charset="utf-8"
y style="text-align: center; background-color: #f0f8ff; ...;"
.
- Como puedes ver los atributos están dentro de los elementos.
- Además los elementos tienen un inicio y un fin marcado por el mismo
tag con un
/
como </html>
, </head>
, </meta>
, </title>
,
</body>
.
- Un elemento puede contener otros elementos como por ejemplo
<head>
que tiene dos elementos: <meta>
y </title>
.
- Además un elemento puede contener simplemente texto como
"Hello world"
dentro de <p>
.
Tu tarea en este laboratorio es crear un módulo en C para trabajar
con HTML y para empezar tendrás que representar la estructura
anterior.
Structs, Union y punteros
Atributos
- Se puede deducir de la descripción informal que un atributo tiene una nombre y un valor.
- Una variable
atr
en C que representa esta información podría declararse de esta forma:
1
|
struct { char *nombre; char *valor; } atr;
|
- Una variable de tipo struct contiene todos los campos
mencionados en el struct y se accede a ellos usando la sintaxis
.
- Así
atr.nombre
y atr.valor
son variables con el nombre y el valor del atributo.
- Los dos campos pueden consultarse y modificarse (pueden aparecer en
la parte izquierda de una asignación).
- Empieza escribiendo un main para probar esto, definiendo una
variable
atr
global o local al main, inicializando sus campos e
imprimiéndolos en la salida estándar.
ini_atributo
- Observa esta función que devuelve un atributo inicializando sus
campos con los parámetros
n
y v
1
2
3
4
5
6
|
struct { char *nombre; char *valor; } ini_atributo(char *n, char *v) {
struct { char *nombre; char *valor; } a;
a.nombre = n;
a.valor = v;
return a
}
|
- Para usarlo bien se podría hacer:
1
2
3
|
struct { char *nombre; char *valor; } atr;
atr = ini_atributo("class", "bg-primary text-primary");
|
- Lamentablemente el compilador de C te va a dar algunos errores relacionados con struct anonymous (!?).
- El error principal es que C no es capaz de entender que los dos structs son iguales
- Imagina que pudieras ¿Puedes escribir una versión en la que
ini_atributo
actualice la
variable en vez de devolver el dato (recuerda intercambiar)?
- Esta sería la cabecera de la función (para poder modificar un dato es necesario pasarlo como referencia, es decir, su puntero):
1
|
extern void ini_atributo(struct { char *nombre; char *valor; } *a, char *n, char *v);
|
- Intenta aplicar lo aprendido al sencillo main anterior.
Etiquetas
- A estas alturas seguro que ya estás cansada de escribir el tipo
1
|
struct { char *nombre; char *valor; }
|
- Para escribir menos tenemos dos opciones
- Se puede poner un nombre a un struct, sin declarar ninguna variable, así:
1
|
struct atr_s { char *nombre, char *valor; };
|
- Oserva que simplemente se ha colocado una etiqueta de nombre
atr_s
(el sufijo _s
es una convención habitual entre muchas
programadoras y programadores para decir que es el nombre de un
struct, podría llamarse atributo_html
o como tú quisieras)
- A partir de ese momento podemos declarar una variable (parámetro o lo que sea) escribiendo
1
|
struct { char *nombre, char *valor; } a;
|
- Prueba a reescribir todo tu programa (sobre todo
ini_atributo
) usando este recurso. ¡Ahora ya podrás compilar!
Typedef
- C permite ponerle tu propio nombre a los tipos
- Por ejemplo, podríamos quere llamar tipo natural a los unsigned int
- La palabra clave para hacerlo es
typedef
- Mira algunos ejemplos (de nuevo el sufijo
_t
es un convención para
decir que esos son nombres de tipos):
1
2
3
|
typedef unsigned int natural_t;
typedef long unsigned int size_t;
typedef char *string_t;
|
- ¿Puedes declarar un tipo,
atributo_t
, para los atributos de HTML?
- Aplícalo a tu main
Elementos
- Vamos con el segundo concepto en HTML: los elementos
- Se puede deducir de la descripción informal que un elemento tiene un
tag, una lista de atributos y una lista de contenidos
(donde un contenido puede ser otro elemento o un texto)
- ¿De qué forma se podría declarar una variable
elem
para
representar dicha información?
- ¿Cómo vas a representar una lista de cosas?
- ¿Cómo vas a representar un contenido (que puede ser un elemento o simplemente texto)?
- Vamos a empezar por lo que quizás no se ha dado en clase: union.
Union y Enum
- La estructura sintáctica de union es como la de struct (se usa
la palabra reservada
union
y tiene varios campos) pero su
significado es completamente diferente: en una union
sólo uno de
los campos es relevante
- Imagina que quieres una variable
medida
que puede contener la medida de un sensor, un dato
entero de temperatura o un dato double de presión pero no los dos a la vez:
1
|
union { int temp; double pres; } medida;
|
- De esta forma se puede trabajar con
medida.temp
si el sensor
es un sensor de temperatura o con medida.pres
si el sensor es
un sensor de presión
- Escribe un programa para probar lo que pasa, entre los experimientos te sugiero:
- Declara la variable
- Rellena el campo
temp
e imprimes los dos campos
- Rellena el campo
pres
e imprimes los dos campos
- ¿Cuál es el tamaño que ocupa esa variable? ¿Por qué?
- C tiene además la posibilidad de definir enumerados
- Los enumerados permiten tener variables restringidas a varios posibles valores
- Por ejemplo la siguiente variable permite representar dos valores,
ELEMENTO
y TEXTO
1
|
enum { ELEMENTO, TEXTO } tipo_de_contenido;
|
- En C, una variable enum es internamente, cómo no, un entero
- Ahora ya conoces todos los conceptos para representar elementos de HTML
- Intenta definir los tipos
contenido_t
y elemento_t
usando un typedef
Un módulo HTML
- Un módulo en C lo componen un header, es decir un fichero
.h
,
con declaraciones de variables globales, funciones, tipos,
etc. y una implementación, es decir, un fichero .c
con las
definiciones de las variables y funciones
- Vamos a implementar un módulo para representar HTML (
html.h
y
html.c
) y un programa principal (test_html
) que será un test de
nuestro módulo
- Empezaremos con el header
html.h
, luego implementaremos los tests test_html.c
y finalmente completaremos la implementación del módulo html.c
- El header contendrá:
- Las definiciones de los tipos
atributo_t
, elemento_t
y contenido_t
- Las cabeceras de las funciones que permiten manejar esos tipos, por ejemplo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
/*
* Dado un nombre y un valor devuelve un nuevo atributo
* NOTA: deberás copiar los strings usando funciones como strndup
*/
atributo_t *crear_atributo(char *nombre, char *valor);
/*
* Dada una lista de atributos y otra de contenidos devuelve un nuevo elemento
*/
elemento_t *crear_elemento(atributo_t *atributos, size_t n_atributos,
contenido_t *contenidos, size_t n_contenidos);
/*
* Devuelve un contenido que es un elemento.
*/
contenido_t *crear_contenido_elem(elemento_t *elemento);
/*
* Devuelve un contenido que es texto.
* NOTA: deberás copiar el string usando funciones como strndup
*/
contenido_t *crear_contenido_texto(char *texto);
/*
* Deja una representación bonita del HTML representado por un elemento
* en un buffer limitado a buffer_size caracteres.
*/
void sprint_elemento(elemento_t *elemento, char *buffer, size_t buffer_size);
|
- Como puedes observar, todo son punteros
- ¿Por qué?
- ¿Qué implicaciones tiene?
- Si hay punteros tiene que haber reserva de memoria ¿Dónde va a
estar?
- Si hay reserva de memoria tiene que haber liberación de memoria ¿Qué
podemos hacer al respecto?
La implementación vacía de html.c
- Antes de hacer los tests, y con el fin de poder compilarlos,
necesitamos una implementación de
html.c
vacía
- Por ejemplo, para
crear_atributo
se puede hacer
1
2
3
4
5
6
7
8
|
atributo_t *crear_atributo(char *nombre, char *valor) {
/*
* Nombramos los parámetros para engañar al compilador
* para que crea que los usamos ;)
*/
nombre; valor;
return NULL;
}
|
El test
- Creamos un fichero
test_html.c
con los includes necesarios y un
main que ejecute y pruebe las funciones del header
- Puedes terminar con un ejemplo que codifique el HTML del principio
La implementación
- Ahora que lo tienes todo compilando, ahora que los tests fallan,
completa las implementaciones de
html.c
paso a paso.
FAQ
Algunas preguntas surgidas en clase:
- Pregunta: ¿Hay alguna forma de inicializar un struct de un solo golpe?
- Respuesta: Sí
1
2
|
atributo_t a;
a = {"class", "bg-primary text-primary"};
|
- Pregunta: ¡Pero si hago esto da error!
1
2
|
atributo_t a;
a = {"class", "bg-primary text-onprimary"};
|
- Respuesta: Pues es verdad pero no se exáctamente porqué. Quizás el compilador no entiende que la expresión
{"class", "bg-primary text-onprimary"}
es un struct (¡de hecho también es un array!). Una forma de resolverlo es ayudar al compilador con un casting:
1
2
|
atributo_t a;
a = (atributo_t){"class", "bg-primary text-onprimary"};
|
- Pregunta: ¿Cómo accedo a un campo de un struct apuntado por un puntero
p
?
- Respuesta: Supongamos
1
2
3
4
|
struct atr_s {char * n; char *v; };
struct atr_s a;
struct atr_s *p;
|
Vamos a exponer MATEMÁTICAMENTE una sintaxis que habla de los tipos de las expresiones:
1
2
3
4
5
6
7
8
9
|
a : struct atr_s (el tipo de a es struct atr_s)
p : struct atr_s *
a.n : char *
*p : struct atr_s
*p.n : char *
(*p).n : char *
p.n : ERROR DE TIPOS
*(p.n) : ERROR DE TIPOS
*p.n == *(p.n)
|
Por suerte C nos regala una bonita sintaxis para acceder al campo de un struct apuntado por p
:
“Last words”
A estas alturas ya te habrás dado cuenta de que las definiciones de
los tipos son “recursivas”, para definir el tipo elemento_t
necesitas contenido_t
y viceversa. Para resolver este problema de
tipos no definidos que te habrás encontrado C te permite anticipar
simplemente los nombres de structs, unions y enums. Veamos la
definición completa de todos los tipos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
/*
* Se pueden anticipar structs con nombre (en este caso ni
* siquiera haría falta por que con el typedef bastaría)
*/
struct atr_s;
struct ele_s;
struct con_s;
/*
* Se pueden usar structs con nombre para declarar nombres de tipos
*/
typedef struct atr_s atributo_t;
typedef struct ele_s elemento_t;
typedef struct con_s contenido_t;
/*
* Y ahora ya se pueden definir los structs incluso "recursivamente"
* (¡elemento_t hace uso de contenido_t y viceversa!)
*/
struct atr_s {
char *nombre;
char *valor;
};
/* Importante: en C siempre necesitamos la longitud de los arrays */
struct ele_s {
char *tag;
size_t n_atributos;
atributo_t *atributos;
size_t n_contenidos;
contenido_t *contenidos;
};
/* Podríamos definir un tipo para este union pero no es necesario */
union elemento_o_texto_u {
char *texto;
elemento_t *elemento;
};
/*
* Podríamos haber definido el enum con una etiqueta tipo_contenido_e
* y luego el typedef usando enum tipo_contenido_e
*/
typedef enum { TEXTO, ELEM } tipo_contenido_t;
struct con_s {
tipo_contenido_t tipo;
union elemento_o_texto_u dato;
};
|