Analiza składni, nazywana parsingiem, jest bardzo często pierwszym etapem przetwarzania. Parsing poprzedza między innymi wyświetlenie dokumentu HTML przez przeglądarkę (parsing dokumentu HTML) oraz stosowanie stylów (parsing dokumentu CSS). W artykule przedstawię przykładową implementację parsingu dokumentu DTD.
Wynikiem przetworzenia dokumentu DTD są pliki XML zawierające wszystkie informacje na temat składni języka HTML 4.01 strict. Opis w języku XML może być wykorzystywany do przygotowania walidatora HTML czy ściągawki zawierającej zestawienie wszystkich elementów i atrybutów języka HTML wraz z ograniczeniami użycia.
Parsing bez zagnieżdżeń
W wielu językach występuje ograniczenie zakazujące zagnieżdżania pewnych konstrukcji. Na przykład w języku HTML nie wolno zagnieżdżać komentarzy:
PRZYKŁAD POPRAWNY
PRZYKŁAD NIEPOPRAWNY
dolor...
-->
Wartości atrybutów HTML należy ująć w cudzysłów, przy czym cudzysłowu nie wolno zagnieżdżać:
PRZYKŁAD POPRAWNY
...
PRZYKŁAD NIEPOPRAWNY
...
Nie wolno również zagnieżdżać znaków {stala}<{/stala}oraz {stala}>{/stala} w dokumencie HTML:
PRZYKŁAD POPRAWNY
lorem
PRZYKŁAD NIEPOPRAWNY
> lorem
komentarzy CSS:
PRZYKŁAD POPRAWNY
body {
/* lorem ipsum dolor... */
}
PRZYKŁAD NIEPOPRAWNY
body {
/* lorem /* ipsum */ dolor... */
}
wpisów ENTITY w dokumentach DTD:
PRZYKŁAD POPRAWNY
<!ENTITY % lorem ...>
PRZYKŁAD NIEPOPRAWNY
<!ENTITY % lorem
<!ENTITY % ipsum >
>
ani komentarzy wewnątrz wpisów DTD:
PRZYKŁAD POPRAWNY
<!ENTITY % lorem --komentarz-->
PRZYKŁAD NIEPOPRAWNY
<!ENTITY % lorem --komentarz --zagnieżdżony-->
Napisy te mogą być jednoznakowe:
\"(.*?)\"
<(.*?)>
wieloznakowe:
--(.*?)--
identyczne:
--(.*?)--
\"(.*?)\"
oraz różne:
<(.*?)>
Tabela 1 przedstawia sumaryczne zestawienie wyrażeń regularnych do parsingu niezagnieżdżonych zapisów wyznaczonych przez napis początkowy i końcowy.
Kwantyfikatory leniwe w wyrażeniach PCRE zapisujemy dodając znak zapytania (np. {stala}*?{/stala}) lub stosując modyfikator U (np. {stala}/…/U{/stala}).
Pamiętajmy, że znaczenie znacznika końcowego może być w wielu językach wyłączone. Tak się dzieje np. w przypadku napisu {stala}?>{/stala}, kończącego skrypt PHP. Znacznik {stala}?>{/stala}, który znajduje się wewnątrz napisu drukowanego instrukcją echo, nie kończy skryptu:
\';
?>
Podobna sytuacja ma miejsce w przypadku znaków {stala}\'{/stala} i {stala}\”{/stala} poprzedzonych backslashem:
echo \'I\\'m a king bee\';
Wyrażenia regularne podane w tabeli 1 wymagają pełnej gwarancji braku zagnieżdżenia, bez żadnych metod cytowania. W odniesieniu do napisów:
START =
\';
?>
PRZYKŁAD NIEPOPRAWNY
WYRAŻENIE REGULARNE:
START = \'
STOP = \'
\'I\\'m a king bee\'
PRZYKŁAD NIEPOPRAWNY
WYRAŻENIE REGULARNE: \'([^\']+)\'
otrzymane wyniki będą błędne.
Jedną z metod umieszczania znacznika końca w analizowanym napisie jest zamiana na inne znaki. W języku HTML znaki {stala}<{/stala} i {stala}>{/stala} są cytowane poprzez zamianę na encję {stala}<{/stala} oraz {stala}>{/stala}.
START = <
STOP = >
PRZYKŁAD POPRAWNY
WYRAŻENIE REGULARNE: <([^>]+)>
W przypadku takich metod cytowania opisana metoda oraz wyrażenia spisują się bez zarzutu.
We wszystkich przypadkach mamy do czynienia z wyodrębnieniem fragmentu rozpoczynającego się i kończącego przez zadane napisy. Napisy te mogą przyjąć postać pojedynczych znaków:
znacznik HTML: <....>
wartość atrybutu HTML: \"...\"
oraz ciągów znaków:
komentarz HTML:
komentarz HTML: /*...*/
wpis DTD: <!ENTITY...>
komentarz wewnątrz ENTITY DTD: --...--
Ponadto napisy otwierający i zamykający mogą być identyczne:
wartość atrybutu HTML: \"...\"
komentarz wewnątrz wpisu DTD: --...--
lub różnić się:
znacznik HTML: <....>
komentarz HTML:
komentarz HTML: /*...*/
ENTITY DTD: <!ENTITY...>
W jaki sposób wyodrębnić fragment tekstu ujęty w napisy, których nie wolno zagnieżdżać? Należy do tego wykorzystać wyrażenia regularne PCRE z zanegowanym zbiorem znaków (gdy napis końcowy jest pojedynczym znakiem) lub z kwantyfikatorem leniwym (gdy znacznik końca jest dłuższym napisem).
Jednoznakowy parsing bez zagnieżdżeń
W celu wyodrębnienia fragmentu napisu określonego przez znak początkowy (np. {stala}<{/stala}) i znak końcowy (np. {stala}>{/stala}) należy użyć wyrażenia regularnego:
<([^>]*)>
W przypadku znaków początkowego i końcowego {stala}\”{/stala} wyrażenie to przyjmie postać:
\"([^\"]*)\"
Ogólnie, wyrażenie regularne jest następujące:
XXX([^Y]*)Y
gdzie {stala}XXX{/stala} jest dowolnym niepustym napisem (napis ten może być wieloznakowy). Natomiast {stala}Y{/stala} jest pojedynczym znakiem.
Wieloznakowy parsing bez zagnieżdżeń
Do wyodrębnienia fragmentu wyznaczonego przez napisy początkowy (np. {stala}{/stala}) należy użyć wyrażenia regularnego:
Kwantyfikator leniwy {stala}*?{/stala} zatrzyma się na pierwszym wystąpieniu napisu końcowego {stala}–>{/stala}.
Z racji na konieczność cytowania znaków {stala}/{/stala} oraz {stala}*{/stala} w przypadku komentarzy CSS wyrażenie to przyjmie postać:
\/\*(.*?)\*\/
Komentarze wewnątrz wpisów DTD pasują do wyrażenia:
--(.*?)--
Zaś wpisy ENTITY do wyrażenia:
<!ENTITY(.*?)>
(Zauważmy, że napis końca jest w tym przypadku jednoznakowy. Dlatego wyrażenie to można zastąpić wyrażeniem z ogranicznikiem jednoznakowym {html}<!ENTITY([^>]*)>{/html}). Ogólnie, wyrażenie regularne przyjmie postać:
START(.*?)STOP
gdzie {stala}START{/stala} i {stala}STOP{/stala} są dowolnymi niepustymi napisami.
Parsing napisu zawierającego kilka rodzajów wpisów
Niech dany będzie napis {stala}$src{/stala}, który należy poddać parsingowi. Napis ten składa się z ciągu wpisów, których nie można zagnieżdżać i które być może są oddzielone białymi znakami. Załóżmy, że każdy z wpisów pasuje do jednego z wyrażeń regularnych: {stala}$re1, $re2, …, $reN{/stala} oraz, że podane wyrażenia regularne rozpoczynają się od znaku {stala}^{/stala} (tj. są zakotwiczone na początku).
Wówczas analiza całego napisu {stala}$src{/stala} z gwarancją wychwycenia wszystkich elementów niepoprawnych przyjmie postać przedstawioną na listingu 1. Oczywiście napis {stala}$src{/stala} może pochodzić z pliku:
$src = trim(file _ get _ contents(\'filename.txt\'));
$re1 = \'/^.../\';
$re2 = \'/^.../\';
...
$reN = \'/^.../\';
while ($src) {
if (preg_match($re1, $src, $m)) {
...//przetwarzanie 1
} elseif (preg_match($re2, $src, $m)) {
...//przetwarzanie 2
} elseif (preg_match($re3, $src, $m)) {
...
} elseif (preg_match($reN, $src, $m)) {
...//przetwarzanie N
} else {
die(\'ERROR\');
}
$src = trim(
preg_replace(
\'/^\' . preg_quote($m[0], \'/\') . \'/\',
\'\',
$src
)
);
}
Struktura dokumentu DTD
Składnia języków HTML oraz XHTML jest formalnie opisana przez dokumenty DTD (ang. Document Type Definition). Pliki DTD są zawarte w specyfikacji języka. Dokument DTD języka HTML 4.01 strict zawiera sześć rodzajów wpisów:
- ENTITY
- ELEMENT
- ATTLIST
- komentarze {stala}{/stala}
- odwołania do plików zewnętrznych ({stala}%HTMLlat1;{/stala}, {stala}%HTMLsymbol;{/stala}, {stala}%HTMLspecial;{/stala})
- wpisy zarezerwowane {html}<![...]>{/html}.
Pierwszy z nich, ENTITY, służy do tworzenia makrodefinicji. Na przykład wpis:
<!ENTITY % fontstyle \"TT | I | B | BIG | . SMALL\">
definiuje makro postaci {stala}%fontstyle;{/stala}. Napisy {stala}%fontstyle;{/stala} zostają rozwinięte do postaci {stala}TT | I | B | BIG | SMALL{/stala}.
Drugi rodzaj wpisów definiuje poszczególne elementy języka HTML. Na przykład wpis:
<!ELEMENT (SUB|SUP) - - (%inline;)* -- subscript, superscript -->
definiuje dwa elementy: {stala}SUB{/stala} oraz {stala}SUP{/stala}.
Wreszcie trzeci rodzaj wpisów, ATTLIST, służy do ustalenia listy atrybutów wybranego elementu. Zapis:
<!ATTLIST (SUB|SUP)
%attrs; -- %coreattrs, %i18n, %events --
>
oznacza, że elementy {stala}SUB{/stala} oraz {stala}SUP{/stala} zawierają atrybuty zdefiniowane encją {stala}%attrs;{/stala}.
Zauważmy, że wewnątrz wpisów ENTITY, ELEMENT, ATTLIST komentarze rozpoczynają się i kończą napisem {stala}–{/stala}:
-- wielolinijkowy komentarz wewnątrz
wpisów ENTITY/ELEMENT/ATTLIST --
W ostatnim etapie przetwarzamy wpisy ATTLIST. Definiują one atrybuty elementów HTML.
Najpierw, w pętli, przetwarzamy wszystkie pliki z folderu {stala}attlist/{/stala}. Zawartość każdego pliku dopasowujemy do wyrażenia regularnego:
$re = \'|
<!ATTLIST\s*
(\S+)\s* #nazwa
(?(?=--)--.*?--\s*) #komentarz
(.*?) #atrybuty
>
|xs\';
Wyrażenie to jest najbardziej skomplikowane. Wynika to z faktu, że niektóre wpisy ATTLIST zawierają w pierwszej linii komentarz:
<!ATTLIST (TH|TD) -- header or data cell --
%attrs; -- %coreattrs, %i18n, %events --
...
a niektóre nie:
<!ATTLIST (%fontstyle;|%phrase;)
%attrs; -- %coreattrs, %i18n, %events --
>
W wyrażeniu regularnym PCRE konstrukcja:
(?(warunek) wyrażenie )
jest dopasowywana warunkowo. Podane wyrażenie pasuje wyłącznie, gdy spełniony jest warunek. W roli warunku natomiast użyte jest pozytywne przewidywanie:
(?=--)
Jeśli kolejnymi dwoma znakami są {html}--{/html} (pozytywne przewidywanie ({html}?=--{/html})), to dopasujemy wyrażenie {html}--.*?--\\s*{/html}. Takie jest znaczenie fragmentu:
(?(?=--)--.*?--\s*)
W ten sposób jednym wyrażeniem regularnym przetwarzamy oba rodzaje wpisów.
Po przetworzeniu plików tekstowych tworzona jest tablica {stala}$attlists{/stala}, w której następnie:
- wymieniamy encje,
- przetwarzamy wpisy w postaci alternatywy {stala}a|b|c{/stala},
- po czym przetwarzamy listy atrybutów.
Atrybuty pasują do jednego z czterech wyrażeń regularnych. Do wyrażenia {stala}$reA{/stala}:
$reA = \'/
^
(\S+)\s+ #nazwa
(\S+)\s+ #typ wartosci
(\S+)\s+ #wartosc domyslna
--(.*)-- #komentarz
/sxU\';
pasują wpisy pełne:
disabled (disabled) #IMPLIED -- unavailable in this context --
Do wyrażenia {stala}$reB{/stala}:
$reB = \'/
^
(\S+)\s+ #nazwa
--(.*?)-- #komentarz
/sx\';
pasują wpisy bez podanych typów wartości oraz wartości domyślnej:
%coreattrs; -- id, class, style, title --
Wyrażenie regularne {stala}$reC{/stala}:
$reC = \"/
^
(\S+)\s+ #nazwa
(\S+)\s+ #typ wartosci
([^\n]+)+ #wartosc domyslna
\n
/sx\";
odpowiada za wpisy bez komentarza:
enctype %ContentType; \"application/x-www- form-urlencoded\"
Zaś wyrażenie regularne {stala}$reD{/stala}:
$reD = \"/
^(\S+)$ #nazwa
/sx\";
odpowiada wpisom krótkim:
<!ATTLIST TITLE %i18n>
Po przeanalizowaniu atrybutów wyrażeniami regularnymi {stala}$reA{/stala}, {stala}$reB{/stala}, {stala}$reC{/stala} oraz {stala}$reD{/stala} otrzymane dane zapisujemy w formacie XML.
Podobnie jak w przypadku wpisów ELEMENT, główna charakterystyka każdego pliku XML jest wyrażona przez klasę: {stala}class=\"single\"{/stala} odpowiada wpisom, które definiują atrybuty pojedynczych elementów HTML, {stala}zaś class=\"multiple\"{/stala} to wpisy definiujące atrybuty wielu elementów naraz. Elementem głównym w pliku XML jest {stala}attlist{/stala}.
Listing 9 ilustruje zawartość XML opisującą atrybuty elementów {stala}DT{/stala} i {stala}DL{/stala}, zaś listing 10 atrybuty elementu {stala}FORM{/stala}.
(DT|DD)
<!ATTLIST (DT|DD)%attrs; -- %coreattrs, %i18n, %events -->
DT
DD
%attrs;
FORM
%attrs;
action
%URI;
#REQUIRED
server-side form handler
method
(GET|POST)
GET
HTTP method used to submit the form
...
Gdy gotowe są cztery skrypty odpowiedzialne za kolejne etapy parsingu, możemy przygotować skrypt realizujący całe przetwarzanie. W skrypcie {stala}parse-dtd.php{/stala} umieszczamy cztery instrukcje {stala}include{/stala} dołączające skrypty odpowiedzialne za poszczególne fazy:
Wywołanie skryptu {stala}parse-dtd.php{/stala} będzie wykonywało kompletny parsing dokumentu DTD języka HTML 4.01 strict.
Podsumowanie
Jaki jest cel opisanego przeze mnie zadania? Widzę dwa poważne zastosowania. Pierwszym z nich jest opracowanie walidatora języka HTML, a drugim - pełnego zestawienia wszystkich elementów i atrybutów wraz z podstawowymi ograniczeniami składniowymi. Zestawienie tego typu jest dostępne pod adresem http://www.eskomo.com/~bloo/indexdot/ html/index.html.
W generowanych dokumentach XML umieszczam zawsze oryginalny wpis z pliku DTD. Pozwoli to na szybsze odnalezienie ewentualnych błędów czy nieścisłości. Pole to wymusza użycie formatu XML zamiast zwykłych plików tekstowych. Dzięki formatowi XML generowane dokumenty są czytelne i mogą zawierać długie fragment tekstu DTD (także ze znakami złamania wiersza).
Zauważmy, że pliki XML opisujące elementy HTML zawierają sekcje {stala}canContain{/stala}:
UL
LI
LI
P
H1
H2
...
Dzięki temu można w prosty sposób stwierdzić czy dany element może być zawarty w innym elemencie oraz wygenerować dwie listy:
- co dany element może zawierać,
- gdzie dany element może być umieszczony.
Dla elementu {stala}LI{/stala} listy te przyjmą postać:
- może zawierać: {stala}P{/stala}, {stala}H1{/stala}, {stala}H2{/stala}, ...,
- może być zawarty w: UL.
Wprawdzie podane listy nie wyczerpują możliwości poprawnego złożenia elementów (m.in. z racji na użycie kwantyfikatorów {stala}?{/stala} czy operatorów {stala}&{/stala} oraz, w DTD), jednak mogą się okazać przydatne przynajmniej do eliminacji zupełnie niepoprawnych złożeń.
Opisane rozwiązanie, po niewielkich modyfikacjach, będzie działało również w odniesieniu do DTD języka XHTML 1.0 strict. Dokumenty DTD języków HTML oraz XHTML są zawarte w specyfikacjach dostępnych na stronach W3C. Dokumentację języka HTML 4.01 znajdziemy pod adresem http://www.w3.org/TR/1999/REC-html401-19991224, zaś języka XHTML 1.0 pod adresem http://www.w3.org/TR/2002/REC-xhtml1-20020801.
Ogólny schemat przetwarzania plików z folderu {stala}entity/{/stala} jest następujący: wyszukujemy wszystkie pliki z folderu {stala}entity/{/stala} i kolejno każdy z nich:
- odczytujemy,
- sprawdzamy czy odczytana zawartość pasuje do wyrażenia regularnego,
- dopasowaną zawartość przekształcamy w tablicę asocjacyjną.
W wyniku wykonania opisanego algorytmu, przedstawionego w skrócie na listingu 3, powstaje tablica asocjacyjna {stala}$entities{/stala}. Tablica ta jest w dalszej części skryptu poddawana kolejnym przekształceniom:
- rozwinięcie encji (np. zastąpienie napisu {stala}%list;{/stala} wartością {stala}OL | UL{/stala}),
- przetworzenie elementów w postaci alternatywy (np. {stala}H1|H2|H3{/stala}) w listę,
- przetworzenie listy atrybutów,
- dodanie atrybutów w postaci alternatywy,
- przetworzenie listy wartości (np. {stala}left|center| right{/stala}),
- ustalenie typu encji,
- zapis XML,
- zapis TXT.
$entities = array();
$pliki = glob(\'entity/*.txt\');
foreach ($pliki as $plik) {
$p = trim(file_get_contents($plik));
if (preg_match($re1, $p, $m)) {
$ent = array();
$ent[\'name\'] = trim($m[1]);
$ent[\'dtd\'] = htmlspecialchars(trim($m[0]));
$ent[\'definition\'] = trim($m[2]);
...
array_push($entities, $ent);
} elseif (preg_match($re2, $p, $m)) {
...
array_push($entities, $ent);
} elseif (preg_match($re3, $p, $m)) {
...
array_push($entities, $ent);
} else {
echo \'***ERROR ***\';
}
}
Wynikiem wykonania tego etapu są dwa pliki tekstowe oraz pliki XML. Pliki tekstowe {stala}entities-dictionary.txt{/stala} oraz {stala}entities-dictionary2.txt{/stala} posłużą do rozwijania encji podczas parsingu wpisów ELEMENT oraz ATTLIS. Pierwszy z nich zawiera wszystkie encje w pełnej postaci i ich rozwinięcie:
%LanguageCode; NAME
%Character; CDATA
%pre.exclusion; IMG|OBJECT|BIG|SMALL|SUB|SUP
%LinkTypes; CDATA
%MediaDesc; CDATA
%URI; CDATA
...
Drugi zawiera encje w postaci skróconej (tj. bez końcowego średnika):
%LanguageCode NAME
%Character CDATA
...
Separatorem kolumn w obu plikach jest znak tabulacji.
Wprawdzie dokumentacja HTML zawiera zalecenie, że encje powinny być zapisywane ze znakiem średnika, jednak w pliku DTD w jednym miejscu znajduje się wyjątek. Jest nim wpis ELEMENT STYLE. Z tego powodu potrzebny jest plik {stala}entities-dictionary2.txt{/stala}.
Pliki tekstowe zawierają jedynie kluczowe informacje o wpisach ENTITY, pozwalające na wymianę encji we wpisach ELEMENT oraz ATTLIST. Komplet informacji jest zawarty w plikach XML. Każdy wpis ENTITY jest opisany przez osobny plik XML. Elementem głównym w dokumentach XML jest element {stala}entity{/stala}. O rodzaju elementu informuje atrybut {stala}class{/stala}. Listingi 4 oraz 5 przedstawiają dwa przykładowe pliki XML. Pierwszy z nich opisuje wpis ENTITY, będący makrodefinicją zapisu list, a drugi stanowi skrócony zapis atrybutów rodziny i18n. (Oba listingi są - dla zwiększenia czytelności - w pewnym stopniu skrócone i zmienione.)
list
<!ENTITY % list \"UL | OL\">
UL | OL
UL
OL
i18n
<!ENTITY % i18n ...>
lang
%LanguageCode;
#IMPLIED
language code
dir
(ltr|rtl)
#IMPLIED
direction for weak/neutral text
Automatyczna kategoryzacja wpisów ENTITY przysporzyła mi nieco kłopotów. Zrezygnowałem z niej. Podział wpisów ENTITY na poszczególne kategorie jest realizowany przy użyciu plików tekstowych. Pliki:
entity-elements.txt
entity-attributes.txt
entity-values.txt
entity-ignore.txt
utworzyłem ręcznie. Definiują one poszczególne grupy wpisów ENTITY.
W tym etapie analizie poddajemy wpisy definiujące elementy HTML. Wpisy możemy podzielić na dwa rodzaje: takie, które definiują pojedynczy element, oraz takie, które definiują wiele elementów.
Wpisem, który definiuje jeden element jest, na przykład, opis elementu {stala}SPAN{/stala}:
<!ELEMENT SPAN - - (%inline;)* -- generic language/style container -->
Natomiast definicja elementów {stala}SUB{/stala} oraz {stala}SUP{/stala} wykorzystuje alternatywę (tj. znak {stala}|{/stala}):
<!ELEMENT (SUB|SUP) - - (%inline;)* -- subscript, superscript -->
Jeszcze bardziej złożonym przypadkiem jest definicja nagłówków:
<!ELEMENT (%heading;) - - (%inline;)* -- heading -->
Analiza tego wpisu wymaga wymiany encji {stala}%heading;{/stala} na jej pełną postać {stala}H1H2|H3|H4|H5|H6|{/stala} opisaną przez:
<!ENTITY % heading \"H1|H2|H3|H4|H5|H6\">
Elementy opisane w DTD pasują do wyrażenia regularnego:
$re = \'|
<!ELEMENT\s*
([^ ]+)\s* #nazwa
([^ ]+)\s* #otw
([^ ]+)\s* #zam
(.*?) #zawartosc
(?:--
(.*?) #komentarz
--)?>
|xs\';
Najpierw w pętli przetwarzamy wszystkie pliki z folderu {stala}element/{/stala}, dopasowując je do podanego wyrażenia regularnego. Wynikiem przetwarzania folderu jest tablica asocjacyjna {stala}$elements{/stala}:
$elements = array();
$pliki = glob(\'element/*.txt\');
foreach ($pliki as $plik) {
$p = trim(file _ get _ contents($plik));
if (preg _ match($re, $p, $m)) {
$el = array();
$el[\'name\'] = trim($m[1])
...
array _ push($elements, $el);
} else {
echo \'*** ERROR ***\';
}
}
Otrzymaną tablicę poddajemy kolejnym etapom przetwarzania:
- wymieniamy encje,
- przetwarzamy nazwy alternatywne (np. {stala}OL | UL{/stala}),
- analizujemy dopuszczalną zawartość (włącznie z wykluczeniami),
- generujemy zapis postaci {stala}a|b|c{/stala} zamiast np. {stala}(LI)+{/stala},
- zapisujemy pliki XML.
Proces wymiany encji jest przedstawiony na listingu 6. Wykorzystujemy do tego przygotowany plik {stala}entities-dictionary.txt{/stala}. Plik wczytujemy ({stala}file_get_contents(){/stala}), kroimy ({stala}string2VArray(){/stala}), po czym w pętli wymieniamy encje ({stala}str_ replace(){/stala}).
$ec = count($elements);
$entities = file_get_contents(\'txt/entities-dictionary.txt\');
$entities = string2VArray($entities, \"\t\");
for ($i = 0; $i < $ec; $i++) {
$elements[$i][\'nameExpanded\'] = trim(
str_replace(
$entities[2][0],
$entities[2][1],
$elements[$i][\'name\']
), \" \r\n\t()\"
);
}
Wynikiem wykonania tego etapu przetwarzania są pliki XML: jeden plik na jeden wpis ELEMENT. Elementem głównym w dokumencie XML jest element. W zależności od tego czy wpis definiuje jeden element, czy wiele, stosujemy klasę {stala}class=\"single\"{/stala} oraz {stala}class=\"multiple\"{/stala}.
Listing 7 przedstawia wygenerowany opis XML elementów {stala}SUP{/stala} oraz {stala}SUB{/stala}, zaś listing 8 opis elementu UL.
(SUB|SUP)
<!ELEMENT (SUB|SUP) - - (%inline;)* ...>
subscript, superscript
false
-
-
(%inline;)*
SUB
SUP
TT
I
B
...
UL
<!ELEMENT UL - - (LI)+ ...>
unordered list
false
-
-
(LI)+
LI
W pierwszej fazie analizy oryginalny plik DTD kroimy na mniejsze fragmenty. Wycinamy z niego wpisy ENTITY, ELEMENT oraz ATTLIST. Wycięte definicje zapisujemy w osobnych plikach tekstowych.
Do sześciu rodzajów wpisów:
- {html}<!ENTITY >{/html}
- {html}<!ELEMENT >{/html}
- {html}<!ATTLIST >{/html}
- komentarz {html}{/html}
- encja zewnętrzna {html}%HTMLlat1;{/html}
- wpis zarezerwowany {html}<![ ]>{/html}
stosujemy sześć wyrażeń regularnych.
Do wpisu ENTITY:
$reEntity = \'|
^
<!ENTITY\s+
[^>]*
>
|Usx\';
Do wpisu ELEMENT:
$reElement = \'|
^
<!ELEMENT\s+
[^>]*
>
|Usx\';
Do wpisu ATTLIST:
$reAttlist = \'|
^
<!ATTLIST\s+
[^>]*
>
|Usx\';
Do komentarzy:
$reComment = \'|
^
|Usx\';
Do zewnętrznych encji:
$reExternal = \'|
^
%\S+;
|Usx\';
Do wpisów zarezerwowanych:
$reBracket = \'|
^
<!\[
.*
\]>
|Usx\';
Każde z powyższych wyrażeń regularnych wykorzystuje fakt, że wpisów nie można
zagnieżdżać.
Schemat przetwarzania skryptu wycinającego wpisy ENTITY, ELEMENT oraz ATTLIST jest zgodny z listingiem 1:
- odczytujemy cały plik DTD i usuwamy wiodące i końcowe białe znaki,
- w pętli, dopóki odczytane DTD jest niepuste:
- ucinamy początkowy fragment pasujący do jednego z sześciu wyrażeń regularnych,
- wycięty fragment ENTITY, ELEMENT lub ATTLIST zapisujemy do pliku, pozostałe ignorujemy,
- usuwamy wiodące białe znaki z DTD.
Jeśli trafimy na fragment, który nie pasuje do żadnego z wyrażeń regularnych, skrypt zgłasza błąd. Taki sposób przetwarzania gwarantuje, że cały plik zostanie przetworzony, każdy z wpisów musi pasować do jednego z wyrażeń regularnych. Kompletny skrypt jest przedstawiony na listingu 2. Wynikiem przetwarzania skryptu są pliki tekstowe, które zostają zapisane w folderach {stala}entity/{/stala}, {stala}element/{/stala} oraz {stala}attlist/{/stala}.
$filename = \'dtd/strict.dtd\';
$p = trim(file_get_contents($filename));
while ($p) {
if (preg_match($reEntity, $p, $m)) {
file_put_contents(\'entity/\' . $i . \'.txt\', $m[0]);
} elseif (preg_match($reElement, $p, $m)) {
file_put_contents(\'element/\' . $i . \'.txt\', $m[0]);
} elseif (preg_match($reAttlist, $p, $m)) {
file_put_contents(\'attlist/\' . $i . \'.txt\', $m[0]);
} elseif (preg_match($reComment, $p, $m)) {
} elseif (preg_match($reExternal, $p, $m)) {
} elseif (preg_match($reBracket, $p, $m)) {
} else {
die(\'ERROR\');
}
$p = trim(
preg_replace(
\'/^\' . preg_quote($m[0], \'/\') . \'/\',
\'\',
$p
)
);
}
Wpisy ENTITY mogą definiować:
- elementy HTML
- atrybuty elementów
- wartości atrybutów
- rodzaj danych (CDATA)
- odwołania zewnętrzne
- wpisy do zignorowania
- wpisy zarezerwowane
- wpisy typu NAME
Najważniejszymi są definicje elementów:
<!ENTITY % heading \"H1|H2|H3|H4|H5|H6\">
definicje atrybutów:
<!ENTITY % coreattrs
\"id ID #IMPLIED -- document- wide unique id --
class CDATA #IMPLIED -- space- separated list of classes --
style %StyleSheet; #IMPLIED -- associated style info --
title %Text; #IMPLIED -- advisory title --\"
>
definicje wartości atrybutów:
<!ENTITY % InputType
\"(TEXT | PASSWORD | CHECKBOX |
RADIO | SUBMIT | RESET |
FILE | HIDDEN | IMAGE | BUTTON)\"
>
oraz definicje rodzaju danych:
<!ENTITY % URI \"CDATA\"
-- a Uniform Resource Identifier,
see [URI]
-->
Wymienione cztery rodzaje encji pasują do wyrażenia regularnego:
$re1 = \'|
<!ENTITY\s+
%\s+
([^ ]+)\s* #nazwa
\"([^\"]+)\"\s* #def
(?:--
(.*?) #komentarz
--)?>
|xs\';
Wpisy odwołujące do zewnętrznych plików pasują do wyrażenia:
$re2 = \'|
<!ENTITY\s+
%\s+
([^ ]+)\s* #nazwa
PUBLIC\s*
\"(.*?)\"\s*
\"(.*?)\"\s* #plik
>
|xs\';
Natomiast wpisy zarezerwowane pasują do wyrażenia:
$re3 = \'|
<!ENTITY\s+
%\s+
reserved\s+
\"\"\s*
>
|xs\';
Każdy z wpisów ENTITY pasuje do jednego z podanych trzech wyrażeń regularnych.