Jakiś czas temu, w trakcie prac nad serwisem webologia.pl postanowiłem przejść z HTML 4.01 Transitional na XHTML 1.0 Strict. Początek pracy był prosty – pozmieniałem literki na małe w nazwach znaczników i atrybutach, pozamykałem znaczniki i przeniosłem elementy prezentacyjne do arkusza CSS. Po wykonaniu tego strona przeszła test poprawności składniowej na walidatorze W3C, ale nadal wymagała jeszcze poprawek ze względu na różnice w interpretacji kodu przez przeglądarki, głównie przez MSIE, który nawet w wersji 7 wciąż nie jest w pełni zgodny ze standardami. Ostatecznie strona jednak zaczęła sensownie wyglądać pod wszystkimi przeglądarkami, więc postanowiłem wykonać kolejny krok, czyli zmienić typ MIME.
Dokumenty XHTML 1.0 powinny być wysyłane z użyciem typu MIME application/xhtml+xml
. Jak zwykle jednak przeglądarka Microsoftu sprawiała problemy – zamiast wyświetlić stronę proponowała zapisanie pliku na dysku. Na szczęście dzięki mechanizmowi negocjacji zawartości (ang. Content Negotiation) można to łatwo obejść. W Internecie można znaleźć wiele gotowych skryptów – ja wykorzystałem ten znajdujący się na stronie Content Negotiation – raz a dobrze. Po jego zastosowaniu strona zmieniła jednak swój wygląd – jak się okazało, zniknęły części strony generowane przez skrypty JavaScript. W szczególności zniknęły reklamy Adsense, poznikały też przykłady z Kursu JavaScript. Po krótkim śledztwie znalazłem przyczynę – zgodnie z zaleceniami skrypty miałem umieszczone wewnątrz komentarzy HTML. W przypadku HTML 4.01 działało to poprawnie, w przypadku XHTML 1.0 już nie – przeglądarka traktowała skrypty jako komentarz, i po prostu ignorowała je. Najprostszym rozwiązaniem było umieszczenie całego skryptu w sekcji CDATA
. Ponieważ mogło to sprawić problemy przeglądarkom które nie wspierają XHTML, więc w zależności od tego czy strona jest wysyłana jako application/xhtml+xml
czy text/html
dałem wysyłanie odpowiednich dodatkowych znaczników:
<?php if ($xhtml) print "<script type=\"text/javascript\"><![CDATA[\n"; else print "<script type=\"text/javascript\"><!--\n"; ?> // Tutaj jest ciało skryptu <?php if ($xhtml) print "//--></script>\n"; else print "]]></script>\n"; ?>
Alternatywnym rozwiązaniem byłoby przeniesienie skryptów do zewnętrznych plików i dołączenie ich do strony, ale uznałem że w moim przypadku albo tego nie mogę zastosować (Adsense), albo nie warto (krótkie skrypty JavaScript umieszczone w Kursie JavaScript.
Po zastosowaniu powyższego fragmentu kodu część skryptów JavaScript zaczęła działać. Niestety nadal nie działały te które korzystały z document.write()
, w szczególności skrypty Adsense. Problem okazał się bardziej poważny – ta funkcja po prostu nie jest dostępna w dokumentach XHTML wysyłanych jako application/xhtml+xml
. W sumie ma to sens – do modyfikacji struktury dokumentu powinny być używane funkcje operujące na drzewie DOM, gdyż użycie funkcji document.write()
może łatwo uczynić dokument niepoprawnym. Na szczęście udało mi się znaleźć rozwiązanie – John Resig umieścił na swoim blogu artykuł zatytułowany XHTML, document.write, and Adsense. W artykule tym opisał że problem ten można rozwiązać korzystając z właściwości innerHTML
. Samo rozwiązanie jest trochę bardziej skomplikowane, gdyż w grę wchodzą tutaj różnice w działaniu różnych przeglądarek. Na szczęście na wspomnianej stronie John umieścił także gotowy skrypt JavaScript. Skopiowałem go do siebie i naniosłem poprawki zasugerowane przez inne osoby w komentarzach do tamtego artykułu. Po dołączeniu tego skryptu do strony reklamy Adsense ponownie się pojawiły. Wciąż miałem jednak problem z niektórymi przykładowymi skryptami JavaScript które umieściłem, więc dokonałem dodatkowych modyfikacji tego skryptu tak aby one także zaczęły działać. Gotowy skrypt z wszystkimi modyfikacjami wygląda następująco:
document.write = function(str){ // convert str to string str += ""; var moz = !window.opera && !/Apple/.test(navigator.vendor); // Watch for writing out closing tags, we just // ignore these (as we auto-generate our own) if ( str.match(/^<\//) ) return; // Make sure & are formatted properly, but Opera // messes this up and just ignores it if ( !window.opera ) str = str.replace(/&(?![#a-z0-9]+;)/g, "&"); // Watch for when no closing tag is provided // (Only does one element, quite weak) // Skip already closed elements if (!str.match(/^<([a-z0-9]+).*<\/\1>$/)) str = str.replace(/<([a-z]+)(.*[^\/])>$/, "<$1$2></$1>"); // Mozilla assumes that everything in XHTML innerHTML // is actually XHTML - Opera and Safari assume that it's XML if ( !moz ) str = str.replace(/(<[a-z]+)/g, "$1 xmlns='http://www.w3.org/1999/xhtml'"); // The HTML needs to be within a XHTML element //var div = document.createElementNS("http://www.w3.org/1999/xhtml","div"); if(document.createElementNS){ var div = document.createElementNS("http://www.w3.org/1999/xhtml", "div") } else { var div = document.createElement("div"); div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); } div.innerHTML = str; // Find the last element in the document var pos; // Opera and Safari treat getElementsByTagName("*") accurately // always including the last element on the page if ( !moz ) { pos = document.getElementsByTagName("*"); pos = pos[pos.length - 1]; // Mozilla does not, we have to traverse manually } else { pos = document; while ( pos.lastChild && pos.lastChild.nodeType == 1 ) pos = pos.lastChild; } // Add all the nodes in that position var nodes = div.childNodes; while ( nodes.length ) pos.parentNode.appendChild( nodes[0] ); };
Gotowy skrypt najlepiej jest umieścić w zewnętrznym pliku i następnie dołączyć do wysyłanego dokumentu jeżeli jest wysyłany jako application/xhtml+xml
:
if ($xhtml) print "<script type=\"text/javascript\" src=\"doc_write.js\"></script>";
Zaprezentowane powyżej rozwiązanie jest to pewna „proteza” która pozwoli na uruchomienie niektórych skryptów JavaScript korzystających z document.write()
. Nie jest to rozwiązanie idealne – fragment kodu który ma być wypisany powinien być poprawnym fragmentem kodu HTML. Dodatkowo powinien on jak najbardziej przypominać XHTML – chodzi głównie o wielkość znaków w nazwach znaczników i atrybutów, zamykanie tych znaczników które trzeba (oraz także można) zamknąć, oraz otaczanie wartości atrybutów cudzysłowami lub apostrofami. Dodatkowo fragment ten powinien dać się łatwo przetworzyć w poprawny fragment drzewa XML, zatem odpada przyrostowe wypisywanie fragmentów dokumentu HTML – tego typu skrypty nie będą działać. Dlatego też najlepszym rozwiązaniem byłoby zmodyfikowanie istniejących skryptów tak aby nie korzystały z document.write()
.
W przypadku gdy jednak nie ma wyjścia i trzeba użyć skryptu który wypisuje kod HTML „po kawałku”, lub też np. zawiera znaczniki pisane dużymi literami, trzeba zastosować dodatkowe rozwiązanie – na czas działania tamtego skryptu trzeba dostarczyć funkcję document.write()
która zapisze wypisywany ciąg do zmiennej, i na koniec ją podmienić z powrotem na oryginalną:
<script type="text/javascript"><![CDATA[ var str = ""; orig_doc_write = document.write; document.write = function(s) { str += s; } ]]></script> <!-- tutaj jest skrypt sprawiajacy problemy --> <script type="text/javascript"><![CDATA[ document.write = orig_doc_write; document.write(str); ]]></script>
W powyższym przykładzie wszystkie wywołania document.write()
pochodzące z zewnętrznego skryptu spowodują tylko dopisanie wartości przekazanego parametru do zmiennej str
. W momencie gdy tamten skrypt zakończy działanie, cały fragment kodu HTML do wypisania jest w tej zmiennej, i można go już wypisać. W tym miejscu można go także poddać dalszej obróbce z wykorzystaniem wyrażeń regularnych, np. zamienić wielkość znaków w nazwach atrybutów na małe.
Oczywiście zaprezentowane tutaj na końcu rozwiązanie powinno być traktowane jako rozwiązanie awaryjne, które można zastosować w sytuacji gdy nie ma już innej możliwości poradzenia sobie z problemem. W każdej innej sytuacji warto natomiast zastosować inne rozwiązania, które są „przyjazne” XHTML.
Komentarze