Как отобразить страницу в UTF-8, несмотря на windows-1251 в HTTP-заголовке
Недавно я закинул на сайт несколько статей — заливал в UTF-8. Кодировка была указана в теге meta, но, взглянув на страницы, я увидел крякозябры: «Р§С‚Рѕ-то случилось.» Оказывается, сайт шлёт HTTP-заголовок Content-Type: text/html; charset=windows-1251 и это на нём никак не отключается. Пользователь может получить читабельный текст — только если догадается вручную переключить кодировку в браузере.
Что делать? Переходить на другой хостинг? Само собой, но пока руки не дошли, хотелось добиться результата тут. Перекодировать тексты? Более достойным и интересным показалось поставить Javascript-«заплатку».
Способа переключить кодировку из Javascript я не нашёл. Остался вариант перекодировать текст скриптом, запускаемым по событию onready документа.
Итак, браузер получает текст в UTF-8, разбивает UTF-последовательности на группы по 8 бит и трактует их как коды символов в кодировке Windows-1251. Чтобы восстановить читаемость текста, нужно получить эти коды, объединить их в UTF-последовательности, а из них — восстановить Unicode-коды символов и вернуть последние посредством числовых ссылок HTML на символы. В этом деле обнаружились несколько закавык.
Во-первых, считывая текст из свойства innerHTML, мы обнаруживаем на месте неразрывного пробела (0xA0) HTML-сущность « ». Нужно её заменять обратно на 0xA0.
Во-вторых, функция charCodeAt возвращает код символа в Unicode, а не в Windows-1251, значит нужно преобразовывать первый во второй. В-третьих, символа с кодом 0x98 в Windows-1251 нет, так что эта функция возвращает для него undefined, это нужно предусмотреть.
В-четвёртых, Internet Explorer и Safari не позволяют поменять заголовок документа через DOM, только через соответствующее свойство документа — но туда нельзя писать числовые ссылки HTML. Для этого случая можно переводить Unicode-коды в шестнадцатеричную систему счисления, записывать их в виде «%код» и пропускать через функцию unescape.
Итоговый код получается таким:
bindReady(
function(){
var Win1251 =
{
0x0:
0x0,
0x1:
0x1,
0x2:
0x2,
0x3:
0x3,
0x4:
0x4,
0x5:
0x5,
0x6:
0x6,
0x7:
0x7,
0x8:
0x8,
0x9:
0x9,
0xA:
0xA,
0xB:
0xB,
0xC:
0xC,
0xD:
0xD,
0xE:
0xE,
0xF:
0xF,
0x10:
0x10,
0x11:
0x11,
0x12:
0x12,
0x13:
0x13,
0x14:
0x14,
0x15:
0x15,
0x16:
0x16,
0x17:
0x17,
0x18:
0x18,
0x19:
0x19,
0x1A:
0x1A,
0x1B:
0x1B,
0x1C:
0x1C,
0x1D:
0x1D,
0x1E:
0x1E,
0x1F:
0x1F,
0x20:
0x20,
0x21:
0x21,
0x22:
0x22,
0x23:
0x23,
0x24:
0x24,
0x25:
0x25,
0x26:
0x26,
0x27:
0x27,
0x28:
0x28,
0x29:
0x29,
0x2A:
0x2A,
0x2B:
0x2B,
0x2C:
0x2C,
0x2D:
0x2D,
0x2E:
0x2E,
0x2F:
0x2F,
0x30:
0x30,
0x31:
0x31,
0x32:
0x32,
0x33:
0x33,
0x34:
0x34,
0x35:
0x35,
0x36:
0x36,
0x37:
0x37,
0x38:
0x38,
0x39:
0x39,
0x3A:
0x3A,
0x3B:
0x3B,
0x3C:
0x3C,
0x3D:
0x3D,
0x3E:
0x3E,
0x3F:
0x3F,
0x40:
0x40,
0x41:
0x41,
0x42:
0x42,
0x43:
0x43,
0x44:
0x44,
0x45:
0x45,
0x46:
0x46,
0x47:
0x47,
0x48:
0x48,
0x49:
0x49,
0x4A:
0x4A,
0x4B:
0x4B,
0x4C:
0x4C,
0x4D:
0x4D,
0x4E:
0x4E,
0x4F:
0x4F,
0x50:
0x50,
0x51:
0x51,
0x52:
0x52,
0x53:
0x53,
0x54:
0x54,
0x55:
0x55,
0x56:
0x56,
0x57:
0x57,
0x58:
0x58,
0x59:
0x59,
0x5A:
0x5A,
0x5B:
0x5B,
0x5C:
0x5C,
0x5D:
0x5D,
0x5E:
0x5E,
0x5F:
0x5F,
0x60:
0x60,
0x61:
0x61,
0x62:
0x62,
0x63:
0x63,
0x64:
0x64,
0x65:
0x65,
0x66:
0x66,
0x67:
0x67,
0x68:
0x68,
0x69:
0x69,
0x6A:
0x6A,
0x6B:
0x6B,
0x6C:
0x6C,
0x6D:
0x6D,
0x6E:
0x6E,
0x6F:
0x6F,
0x70:
0x70,
0x71:
0x71,
0x72:
0x72,
0x73:
0x73,
0x74:
0x74,
0x75:
0x75,
0x76:
0x76,
0x77:
0x77,
0x78:
0x78,
0x79:
0x79,
0x7A:
0x7A,
0x7B:
0x7B,
0x7C:
0x7C,
0x7D:
0x7D,
0x7E:
0x7E,
0x7F:
0x7F,
0x402:
0x80,
0x403:
0x81,
0x201A:
0x82,
0x453:
0x83,
0x201E:
0x84,
0x2026:
0x85,
0x2020:
0x86,
0x2021:
0x87,
0x20AC:
0x88,
0x2030:
0x89,
0x409:
0x8A,
0x2039:
0x8B,
0x40A:
0x8C,
0x40C:
0x8D,
0x40B:
0x8E,
0x40F:
0x8F,
0x452:
0x90,
0x2018:
0x91,
0x2019:
0x92,
0x201C:
0x93,
0x201D:
0x94,
0x2022:
0x95,
0x2013:
0x96,
0x2014:
0x97,
0x2122:
0x99,
0x459:
0x9A,
0x203A:
0x9B,
0x45A:
0x9C,
0x45C:
0x9D,
0x45B:
0x9E,
0x45F:
0x9F,
0xA0:
0xA0,
0x40E:
0xA1,
0x45E:
0xA2,
0x408:
0xA3,
0xA4:
0xA4,
0x490:
0xA5,
0xA6:
0xA6,
0xA7:
0xA7,
0x401:
0xA8,
0xA9:
0xA9,
0x404:
0xAA,
0xAB:
0xAB,
0xAC:
0xAC,
0xAD:
0xAD,
0xAE:
0xAE,
0x407:
0xAF,
0xB0:
0xB0,
0xB1:
0xB1,
0x406:
0xB2,
0x456:
0xB3,
0x491:
0xB4,
0xB5:
0xB5,
0xB6:
0xB6,
0xB7:
0xB7,
0x451:
0xB8,
0x2116:
0xB9,
0x454:
0xBA,
0xBB:
0xBB,
0x458:
0xBC,
0x405:
0xBD,
0x455:
0xBE,
0x457:
0xBF,
0x410:
0xC0,
0x411:
0xC1,
0x412:
0xC2,
0x413:
0xC3,
0x414:
0xC4,
0x415:
0xC5,
0x416:
0xC6,
0x417:
0xC7,
0x418:
0xC8,
0x419:
0xC9,
0x41A:
0xCA,
0x41B:
0xCB,
0x41C:
0xCC,
0x41D:
0xCD,
0x41E:
0xCE,
0x41F:
0xCF,
0x420:
0xD0,
0x421:
0xD1,
0x422:
0xD2,
0x423:
0xD3,
0x424:
0xD4,
0x425:
0xD5,
0x426:
0xD6,
0x427:
0xD7,
0x428:
0xD8,
0x429:
0xD9,
0x42A:
0xDA,
0x42B:
0xDB,
0x42C:
0xDC,
0x42D:
0xDD,
0x42E:
0xDE,
0x42F:
0xDF,
0x430:
0xE0,
0x431:
0xE1,
0x432:
0xE2,
0x433:
0xE3,
0x434:
0xE4,
0x435:
0xE5,
0x436:
0xE6,
0x437:
0xE7,
0x438:
0xE8,
0x439:
0xE9,
0x43A:
0xEA,
0x43B:
0xEB,
0x43C:
0xEC,
0x43D:
0xED,
0x43E:
0xEE,
0x43F:
0xEF,
0x440:
0xF0,
0x441:
0xF1,
0x442:
0xF2,
0x443:
0xF3,
0x444:
0xF4,
0x445:
0xF5,
0x446:
0xF6,
0x447:
0xF7,
0x448:
0xF8,
0x449:
0xF9,
0x44A:
0xFA,
0x44B:
0xFB,
0x44C:
0xFC,
0x44D:
0xFD,
0x44E:
0xFE,
0x44F:
0xFF
}
String.prototype.Win1251_charCodeAt=function(char_num){
var char_code=this.charCodeAt(char_num);
return (char_code===undefined)?0x98:Win1251[char_code];
}
function utf8_decode(text){
text=text.replace(/ /g,"\u00A0");
var char_code, char_code2, char_code3, char_code4;
var result_str='';
for(var char_num=0; char_num<text.length; char_num++)
if((char_code=text.Win1251_charCodeAt(char_num))<0x80 || char_code===text.charCodeAt(char_num)) result_str+=text.charAt(char_num);//0zzzzzzz - 00000000 00000000 00000000 0zzzzzzz else if(char_code>=0xC0)
if(char_code<0xE0){
if(
(char_code2=text.Win1251_charCodeAt(++char_num))>=0x80 &&
char_code2<0xC0
)//110yyyyy 10zzzzzz - 00000000 00000000 00000yyy yyzzzzzz result_str+="&#"+((char_code-0xC0)*0x40+(char_code2-0x80))+";";
}
else if(char_code<0xF0){
if(
(char_code2=text.Win1251_charCodeAt(++char_num))>=0x80 &&
char_code2<0xC0 &&
(char_code3=text.Win1251_charCodeAt(++char_num))>=0x80 &&
char_code3<0xC0
)//1110xxxx 10yyyyyy 10zzzzzz - 00000000 00000000 xxxxyyyy yyzzzzzz result_str+="&#"+((char_code-0xE0)*0x1000+(char_code2-0x80)*0x40+(char_code3-0x80))+";";
}
else if(
char_code<0xF8 &&
(char_code2=text.Win1251_charCodeAt(++char_num))>=0x80 &&
char_code2<0xC0 &&
(char_code3=text.Win1251_charCodeAt(++char_num))>=0x80 &&
char_code3<0xC0 &&
(char_code4=text.Win1251_charCodeAt(++char_num))>=0x80 && char_code4<0xC0
)//11110www 10xxxxxx 10yyyyyy 10zzzzzz - 00000000 000wwwxx xxxxyyyy yyzzzzzz result_str+="&#"+((char_code-0xF0)*0x40000+(char_code2-0x80)*0x1000+(char_code3-0x80)*0x40+(char_code4-0x80))+";";
return result_str;
}
function unescapeTitle(title){
return unescape(
utf8_decode(document.title).replace(
/&#([0-9]+);/g,
function(expression, value){
if(isNaN(value=parseInt(value, 10)))
return NaN;
var i=0, retval="", radix=16; while(i++<4 || value>0){
retval=["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"][value%radix]+retval;
value=Math.floor(value/radix);
}
return "%u"+retval;
}
)
);
}
document.body.innerHTML=utf8_decode(document.body.innerHTML);
if("vendor" in navigator && navigator.vendor.indexOf("Apple")>-1){
document.title=unescapeTitle(document.title);
return;
}
/*@cc_on
document.title=unescapeTitle(document.title);
return;
@*/
document.getElementsByTagName("title")[0].innerHTML=utf8_decode(document.title);
}
);
Проверено в Firefox 3 и 4; Opera 9, 10 и 11; Internet Explorer 5.5, 6, 7, 8; Google Chrome и Safari последних релизных версий.
Конечно, это кунштюк; я на нём разобрался, что такое UTF-8. По-хорошему, сервер не должен вредить своими HTTP-заголовками. Но тут встаёт философский вопрос: а кому лучше знать кодировку документа — серверу (автору .htaccess) или самому HTML-документу (его автору)? Возможно, у браузеров есть веская причина верить серверу, а не meta-тегу в документе.
Created/Updated: 25.05.2018