программирование

Воздушный бой на чистом HTML

Воздушный бой (51.63КиБ)

Все в последнее время настолько в восторге от новых возможностей браузеров, что пишут игру за игрой на HTML5/CSS/JavaScript. Это всё скучно. Вчера перед сном я размышлял об игре на чистом HTML (даже без CSS), а сегодня, пока собирался в Самару, на 404fest, потихоньку её написал.

Встречайте — Pure HTML Air Fight. Написано на чистом HTML, без использования JavaScript, CSS и серверной логики. На сервере отрабатывает только скрипт, контролирующий введённый URL и запускающий игру, если URL ошибочен.

Задача классическая — попасть в пролетающий самолёт, внизу есть кнопка, запускающая ракету. Скорость самолёта и задержка меняется (на самом деле они жёстко зависят от момента пуска ракеты, у меня же чистый HTML, никакого рандома).

Кстати, в игре есть фоновый звук при попадании в самолёт.

Добавлено позднее: сайт под «хабрэффектом», так что может открываться крайне медленно, игра может вообще не работать.

Добавлено ещё позднее: некоторые ребята (в том числе в комментариях на «Хабре») почему-то подумали, что у меня на сервере есть какая-то логика. Да нет никакой, кроме явно описанной в тексте. У меня просто куча HTML лежит в папке.

17 комментариев

Как преобразовать пробелы в табуляции в вашей программе

По code guidelines PHPbb в коде нужно использовать табуляции, вместо пробелов. К сожалению, я об этом поздновато узнал, теперь надо всё, что я написал привести к правильному виду.

Оказывается, в Linux есть специальная утилита для этого — unexpand (находится в пакете coreutils). Кстати, есть и противоположная по действию утилита — expand.

19 комментариев

PHP и UTF-8: пятый с половиной этап

После последнего, на данный момент, пятого этапа, меня опять потянуло на нецелые числа. Что поделать — я работаю над другим проектом, поэтому на полноценный этап я ничего не наскрёб, но работу потихоньку продолжаю. В прошлый раз я занимался оптимизацией — сделал итератор по UTF-8 строке.

Так, и?
Тогда же я упомянул, что мне понадобится ещё один итератор, чтобы итерировать строку с конца. Сегодня я его написал, решил выложить, наверняка ещё кому-то понадобится.

<?
    class UtfReverseString implements Iterator
    {
        protected $str;   // строка
        protected $idx;   // бинарный индекс строки
        protected $pos;   // UTF-8-индекс
        protected $curend;  // бинарный указатель на конец текущего символа

        protected $idx_start; // бинарный индекс откуда стартуем
        protected $pos_start; // UTF-8-индекс откуда стартуем

        // проверка — байт не является началом символа
        protected function isNotBegin($ch)
        {
            return (ord($ch) & 192) == 128;
        }

        public function __construct($str, $pos_start = 0)
        {
            $len = strlen($str);

            $this->str = $str;
            $this->idx = strlen($str); // этот указатель будет уточнён тут же, ниже
            $this->pos = strlen(utf8_decode($str)) - 1 - $pos_start; // лучше использовать mb_strlen, если есть возможность

            while ($pos_start-- >= 0) {
                $this->curend = --$this->idx;

                while ($this->idx > 0 && $this->isNotBegin($this->str[$this->idx])) {
                    $this->idx--;
                }
            }

            $this->idx_start = $this->idx;
            $this->pos_start = $this->pos;
        }

        public function current()
        {
            return substr($this->str, $this->idx, $this->curend - $this->idx + 1);
        }

        public function key()
        {
            return $this->pos;
        }

        public function next()
        {
            $this->pos--;
            $this->curend = $this->idx - 1;

            for ($this->idx--; $this->isNotBegin($this->str[$this->idx]); $this->idx--);
        }

        public function rewind()
        {
            $this->idx = $this->idx_start;
            $this->pos = $this->pos_start;
        }

        public function valid()
        {
            return $this->idx >= 0;
        }

        public function __toString()
        {
            return $this->str;
        }
    }

Использовать так же просто, как и предыдущий, «прямой» итератор:

$str = new UtfReverseString('Привет', 1);

    foreach ($str as $ch) {
        echo $ch, ' ';
    }

Второй параметр указывает с какого символа (с конца) нужно начинать итерироваться.

Оставить комментарий

Optkit: можно ли автоматизировать мою графику?

Скриншот PowerShell с выводом таблицы сравнения размеров файлов (0.png, 1.jpg, 2.gif, 3.gif) и заметкой о конвертации GIF в PNG.

Выкладываю optkit — программу (написана на PowerShell v1, это значит — под Windows), которая определяет можно ли оптимизировать графику в форматах PNG, GIF, JPEG в указанной папке. При запуске нужно указать папку.

Примерный вывод есть на скриншоте. Выводится сколько удалось сэкономить, для GIF выводится сколько они будут занимать в PNG (если это даст экономию).

Программа использует ряд других программ, которые должны лежать в папке utils, это:

  • SuperGIF 1.5 (должен быть «купленным»)
  • GIFLite (можно поискать FTP-поиском), утилита не используется для 64-битных ОС
  • jpegtran
  • optipng
  • pngout (нужна command line версия)
    Моя программа не использует никаких продвинутых техник (например, не пытается избавиться от прозрачных, но не одноцветных участков), может ещё руки дойдут и сделаю.

Дополнено: Харисов подсказывается, что на Bash есть инструмент интереснее — imgcomp и писался явно не пару вечеров, как мой. Кстати, сравнивая две программы (на Bash и PowerShell) можно сделать интересные выводы о читаемости этих языков и их мощности.

5 комментариев

PHP4 без XML-парсера, плюс XPath

Я недавно написал простенький парсер XML для PHP 4.3+, для случаев, когда никакого встроенного парсера XML на хостинге не оказалось.

А вчера мне подумалось, что парсить XML в дерево это, конечно, замечательно, но пользоваться этим всё равно неудобно. Было бы неплохо дать возможность получать произвольные узлы так же просто, как это делается при помощи XPath.

Весь XPath я, конечно, реализовывать не буду, да мне столько и не надо. В общем, написал класс, который используется в паре VerySimple_XMLParser. Реализованы получение потомков тега, «*», «node()» и «text()». Это всё, но мне хватит.

// Simple XPath for VerySimple_XMLParser
// /
class VerySimple_XMLParser_Tree
{
    var $path;
    var $counter;
    var $xpath;
    
    function VerySimple_XMLParser_Tree()
    {
        $this->path = array('/');
        $this->counter = 0;
        $this->xpath = array();
    }


    function collect($type, $args)
    {
        $path = implode($this->path);

        switch ($type) {
            case 'TEXT':
                $this->xpath[$path . $this->counter . '/text()/'] = $args;
                $this->counter++;
                break;

            case 'TAGOPEN':
                list($name, $attributes) = $args;

                $tag = $this->counter . '/' . $name . '/';

                $this->xpath[$path . $tag] = $attributes;
                $this->counter = 0;

                $this->path[] = $tag;
                break;

            case 'TAGCLOSE':
                $this->counter = 1 + (int) array_pop($this->path);
                break;
        }
    }

    function halfxpath($path)
    {
        $keys = array_keys($this->xpath);
        $trans = array(
            '\/'       => '\/\d+\/',
            '\*'       => '[^\/()]+',
            'node\(\)' => '[^\/]+',
        );

        $filter   = strtr(preg_quote($path, '/'), $trans);
        $filtered = preg_grep("/^$filter\/$/su", $keys);

        $nodes = array();

        foreach ($filtered as $key) {
            $nodes[$key] = $this->xpath[$key];
        }

        return $nodes;
    }
}

// а вот так это можно использовать:
$xml =<<<XML
<root>
    <a>
        <b><c>Xa!</c></b>
        <b>Hello</b>
    </a>
    <a name="value"/>
</root>
XML;

$tree = &new VerySimple_XMLParser_Tree();

$sax = &new VerySimple_XMLParser(array(&$tree, 'collect'));
$sax->parse($xml);

echo '<plaintext>';
var_dump($tree->halfxpath('/*/a/b/text()'));
var_dump($tree->halfxpath('/*/a/b/node()'));
var_dump($tree->halfxpath('/*/a/b/*'));
var_dump($tree->halfxpath('/root/a'));

Метод для запросов я назвал «halfxpath», намекая, что это не настоящий XPath, а лишь неполноценная эмуляция.

Основная идея, как видно из кода — не парсить XML в дерево, а помещать каждый элемент по специального вида пути, который потом будет проверен на совпадение по регулярному выражению, приготовленному из запроса.

5 комментариев

Тонкости heredoc в PHP

В PHP, как и в некоторых других языках (Bash, Perl) есть конструкция heredoc — это способ задать многострочную строку. Посмотрите как удобно, не нужно экранировать кавычки:

// это PHP
$str = <<<STR
Привет, я строка "в кавычках",
'в апострофах'.
STR;

Но меня всегда раздражало, что завершающий ограничитель приходится прижимать к левому краю, пробел и табуляция дают ошибку (хотя в Perl относительно недавно разрешили делать отступы для этой конструкции).

Сегодня, в очередной раз раздражаясь по этому поводу, я решил попробовать использовать неразрывные пробелы (Alt Right+Space в раскладке Бирмана), получилось, работает даже в PHP 4.4.4. Подходит, конечно, для исходников в кодировке UTF-8. Надеюсь, вы в другой кодировке их уже и не пишете.

Правда, строки внутри, конечно, менять нельзя, но эта конструкция часто используется вместе с HTML, а ему отступы неинтересны.

$str = <<<    HTML
    <table>
        <tr class="row"><td>'Привет'</td></tr>
    </table>
    HTML;

Только не забудьте, что отступы надо делать и в начальной конструкции, и в конечной.

13 комментариев

Bitrix WTF

Я сейчас в Ярославле слушаю доклад по клиентским сертификатам.

Рядом со мной сидит Коля Мациевский, он один из первых в России начал комплексно исследовать вопрос оптимизации загрузки сайтов и владеет сейчас первой в России компанией «WeBo», которая этим занимается. Коле скоро выступать, а пока он ковыряет исходники своего продукта, а точнее плагина, который встраивает упомянутый продукт в CMS «Bitrix».

Причина в том, что внутренний антивирус «Bitrix» почему-то вырезает JavaScript, который подключается плагином. Проблема оказалась в следующем коде:

$ll = strlen(bin2hex($this->body))/2;

if($ll > 500)
{
    $val = 9;
    $r += $val;
    $this->resultrules['rulescriptlenghts_sl'] = $val;
    return $r;//только одно правило из блока
}

Вот эта странная во всех отношениях (почему не просто strlen?) проверка считает вирусным любой JS-код, строка которого занимает больше, чем 500 байт.

4 комментария

IE9 и его скорость, по сравнению с другими браузерами

Сижу сейчас на семинаре Михаила Черномордикова из Microsoft «HTML5, CSS3 и новый Internet Explorer 9». На экране — известный тест Flying Images, который Михаил демонстрирует в IE9 бета1 и FF 4 (неизвестной) бете.

У IE9, судя по всему сильное преимущество. Я как-то засомневался, помню на «Хабре» был материал, из которого следовало прямо обратное — как только в FF4 появилось аппаратное ускорение, IE9 остался далеко позади.

Скачал FF 4 beta7pre, включил в конфигурации всё, что относится к аппаратному ускорению, потестировал. В общем, Microsoft… вводит нас в заблуждение (как и статья на «Хабре»). FireFox явно впереди, хоть и незначительно.

33 комментария

PHP4 без XML-парсера

Мне тут по работе пришлось решить странную, по нынешним временам задачу — принимать XML в программе на PHP4.3+, где никакого парсера XML может не оказаться. Таковы реалии продукта и среды, где он будет использоваться (это код внешнего проекта). Поэтому мне пришлось написать небольшой парсер, который, правда, расcчитывает, что XML будет well-formed (я его получаю из доверенного источника).

Дальше
Я далёк сейчас от shared-хостингов, но знаю, что до сих пор встречаются самые странные конфигурации, возможно кому-то пригодится, потому выкладываю.

<?
// Very Simple XML SAX Parser by Evgeny Stepanischev
// /
class VerySimple_XMLParser
{
    var $saxCallback;

    function VerySimple_XMLParser($saxCallback)
    {
        $this->saxCallback = $saxCallback;
    }

    function parse($xml)
    {
        if (preg_match_all('#<![CDATA[.*?]]>|</?[^>]*>|[^<]+#us', $xml, $matches)) {
            foreach ($matches[0] as $match) {
                $this->parseThis($match);
            }
        }
    }

    function parseThis($match)
    {
        $calls = array(
            '<!--'      => 'COMMENT',
            '<?'        => 'PI',
            '<![CDATA[' => 'CDATA',
            '<!'        => 'DTD',
            '</'        => 'TAGCLOSE',
            '<'         => 'TAGOPEN',
        );

        foreach ($calls as $need => $type) {
            if (strpos($match, $need) === 0) {
                return $this->parseByType($type, $match);
            }
        }

        return $this->parseByType('OTHER', $match);
    }

    function parseByType($type, $text)
    {
        if ($type == 'COMMENT') {
            return $this->addChunk($type, substr($text, 4, -3));
        }

        if ($type == 'CDATA') {
            return $this->addChunk('TEXT', substr($text, 9, -3));
        }

        if ($type == 'OTHER') {
            return $this->addChunk('TEXT', $text);
        }

        if ($type == 'TAGCLOSE') {
            $end = strcspn($text, ' >');

            return $this->addChunk('TAGCLOSE', substr($text, 2, $end - 2));
        }

        if ($type == 'TAGOPEN') {
            $this->addChunk('TAGOPEN', $this->parseTag($text));

            if (substr($text, -2) == '/>') {
                $end = strcspn($text, ' >');

                return $this->addChunk('TAGCLOSE', substr($text, 1, $end - 2));
            }
        }

        return '';
    }

    function parseTag($text)
    {
        $end = strcspn($text, ' />');
        $name = substr($text, 1, $end - 1);

        return array($name, $this->parseTagAttributes(substr($text, $end)));
    }

    function parseTagAttributes($text)
    {
        $nameStart = '(?:[A-Za-z_:]|[^\x00-\x7F])';
        $nameChar  = '(?:[A-Za-z0-9_:\.-]|[^\x00-\x7F])';
        $attrValue = '(?:\'[^\']*\'|"[^"]*")';

        if (preg_match_all("/{$nameStart}{$nameChar}*=$attrValue/us", $text, $matches)) {
            return $this->parseAttributes($matches[0]);
        }

        return array();
    }

    function parseAttributes($rawAttributes)
    {
        $attributes = array();

        foreach ($rawAttributes as $rawAttr) {
            list($name, $value) = explode('=', $rawAttr, 2);
            if ($value[0] == '"' || $value[0] == "'") {
                $value = substr($value, 1, -1);
            }

            $attributes[$name] = $value;
        }

        return $attributes;
    }

    function addChunk($type, $arg)
    {
        call_user_func($this->saxCallback, $type, $arg);
    }
}

// Пример использования

$xml =<<<XML
<!-- комментарий -->
<?xml version="1.0"?>
<root attribute="a.a" v="a">
    <v><![CDATA[data]]></v>
    <v>data</v>
    <v/>
</root>
XML;

// Понятно, что можно написать класс и передать в качестве
// callback в парсер метод объекта, но я для простоты, как пример,
// написал очень простую функцию
function VerySimple_XMLParser_Collector()
{
    static $tree = array();

    if (func_num_args() == 2) {
        $tree[] = func_get_args();
    } else {
        return $tree;
    }
}

$sax = &new VerySimple_XMLParser('VerySimple_XMLParser_Collector');
$sax->parse($xml);

echo '<plaintext>';
var_dump(VerySimple_XMLParser_Collector());

В качестве иллюстрации я использую в коде крайне простую функцию, которая не строит дерево, а просто собирает вызовы в массив, но понятно, что собрать дерево тоже ничего не стоит.

11 комментариев

PHP и UTF-8: пятый этап

Итак, пятый этап. Точнее на этап, он, пожалуй, не тянет, но после этапов 4,5 и 4,6 мне хочется целых чисел.

Итак, пятый этап
Я заметил, что у меня, в моих аналогах строковых функций, умеющих работать с UTF-8, очень много последовательных проходов по строке. После Пайтона, с его итераторами и генераторами, у меня руки чесались оптимизировать это место.

В самом деле, если простое взятие символа по индексу в строке с фиксированным размером символа имеет сложность O(1), то в UTF-8 строке эта сложность возрастает до O(n). Соответственно, когда я прохожу циклом по строке с фиксированным размером символа эта сложность — O(n), а текущие циклы в моих функциях проходят строку UTF-8 за O(n²). Особенно это будет заметно в функции rtrim.

Я написал небольшие тесты. Замерял три прохода по тексту объёмом 187,5КБ. Проход по строке с однобайтной кодировкой — 0,2 секунды, по строке UTF-8 — 160 секунд. Забегая вперёд, скажу, что проход той же строки (UTF-8) итератором занимает 4,3 секунды, результат хуже, чем у встроенных функций, но в 37 раз лучше (на этих данных), чем проход прежним алгоритмом.

Вообще-то в PHP5 появилось очень много вещей, которые никто не использует. Свежие книжки ещё не появились, а документацию никто ни читает. Поэтому один из моих любимых вопросов на собеседовании по PHP — «что бы вы сделали, если бы вам в PHP понадобился двусвязанный список» (что-что, использовал бы SplDoublyLinkedList), впрочем если претендент не отвечает, в минус ему это не идёт, зато правильный ответ идёт в плюс.

Так вот, одна из вещей, которой мало кто пользуется — возможность создать в PHP свой итератор. У меня тут не руководство по итераторам, поэтому я ограничусь лишь кодом и примером использования. Вообще-то мне в коде понадобятся два итератора (второй будет итерировать строку с конца), но они очень похожи, поэтому я приведу только один.

Основная идея — если мы двигаемся по строке последовательно, где-то сохранять текущую позицию, тогда к следующему символу нам перейти будет проще (сложность — O(n)).

UTF-8 достаточно хорошо устроен — первый байт символа всегда можно отличить по виду от его хвоста. У «хвоста» старшие биты всегда выглядят как «10», вот и весь критерий. Так что мы всегда можем сказать где начался новый символ — там где старшие биты байта не совпали с «10».

class UtfString implements Iterator
    {
        protected $str;   // строка
        protected $idx;   // бинарный индекс строки
        protected $pos;   // UTF-8-индекс
        protected $len;   // бинарная длина
        protected $next;  // бинарный указатель на следующий

        protected $idx_start; // бинарный индекс откуда стартуем
        protected $pos_start; // UTF-8-индекс откуда стартуем

        // проверка — байт не является началом символа
        protected function isNotBegin($ch)
        {
            return (ord($ch) & 192) == 128;
        }

        public function __construct($str, $pos_start = 0)
        {
            $this->str = $str;
            $this->pos = $this->pos_start = $pos_start;
            $this->len = strlen($str);
            $this->next = null;

            if ($pos_start) {
                for ($i = 0; $i<$pos_start; $i++) {
                    $this->idx_start++; 

                    while ($this->isNotBegin($this->str[$this->idx_start])) {
                        $this->idx_start++;
                    }
                }
                $this->idx = $this->idx_start;

            } else {
                $this->idx = $this->idx_start = 0;
            }
        }

        public function current()
        {
            $idx = $this->idx;
            $out = $this->str[$idx++];

            while ($this->isNotBegin($ch = $this->str[$idx])) {
                $out .= $ch; // быстрее, чем массив!
                $idx++;
            }

            $this->next = $idx;

            return $out;
        }

        public function key()
        {
            return $this->pos;
        }

        public function next()
        {
            $this->pos++;

            if ($this->next) {
                $this->idx  = $this->next;
                $this->next = null;
            } else {
                for ($this->idx++; $this->isNotBegin($this->str[$this->idx]); $this->idx++);
            }
        }

        public function rewind()
        {
            $this->idx = $this->idx_start;
            $this->pos = $this->pos_start;
        }

        public function valid()
        {
            return $this->idx < $this->len;
        }

        public function __toString()
        {
            return $this->str;
        }
    }

Использовать итератор достаточно просто:

$str = str_repeat('Привет', 64000);

    foreach (new UtfString($str) as $position => $char) {
        // …
    }

Так как в итерации сначала вызывается «current», а потом «next» мне показалось странным терять информацию о положении следующего символа, которую мы в «current» уже знаем, так что я передаю её в «next» через защищённое свойство.

Список функций (из моего списка используемых функций), которые я перепишу с итератором (или скорость которых увеличится): trim, ltrim, rtrim, wordwrap, strcspn и strspn, а так же некоторые места в коде проекта.

10 комментариев

Хром → Опера

К сожалению, с «Хрома» (это браузер), к которому я уже привык и который обставил с любовью плагинами, мне придётся пересесть куда-то ещё. Особенно я привык к расширению, которое позволяет в моём «Андроиде» (это операционная система в телефоне) мгновенно открыть ту же страницу, что у меня в браузере и продолжить читать с того же места. Очень удобно.

Но. Недавняя неприятная история, когда в код «Хромиума» (это «Хром», но без «жучков») попал один из «шпионских» модулей, заставляет задуматься — что ещё, по небрежности или умышленно, могла забыть выключить корпорация «Гугл» в своём браузере? Тем более, что ещё на слуху история о том, что «Гугл» собирал данные WiFi-трафика при помощи автомобилей Street View.

Вообще-то, каких-то особых секретов персонально у меня нет, но они есть у «Яндекса» и мне очень не хочется, чтобы какие-то данные из интранета по моей вине попали в «Гугл». Так что, «Хром» и «Хромиум» я больше не использую. По крайней мере, в ближайшем будущем.

Перехожу обратно на «Оперу», других вариантов нет. «Сафари» под Windows — это какой-то чёрный юмор, FireFox похож на обросшую ракушками баржу, IE9 пока ещё сырой чуть более, чем полностью. Так что, при всё богатстве выбора, другой альтернативы нет.

30 комментариев

PHP и UTF-8: четыре и шесть десятых или ещё проблемные функции

С прошлого этапа я, в данном проекте, был занят тем, что писал UTF-8-аналоги для используемых строковых функций. Всплыли некоторые проблемы, о некоторых я не думал, для других до этого момента не продумывал пути решения.

Дальше
Для начала, у нас в проекте используются ord и chr — функции получения кода символа и получения символа по его коду. Понятно, что места, в которых эти функции используются жёстко завязаны на однобайтные кодировки. В некоторых случаях ничего страшного не случится, в других эти места придётся переписать. Я нашёл несколько случаев использования, переписать пришлось пока только одно место, ещё одно я отложил на будущее. Сами функции я переписал следующим образом:

static public function ord($character) {
        if (self::ON) {
            $codes = unpack('N', mb_convert_encoding($character, 'UCS-4BE', 'UTF-8'));
            return reset($codes);
        }

        return ord($character);
    }

    static public function chr($ascii) {
        if (self::ON) {
            return mb_convert_encoding(pack('N', $ascii), 'UTF-8', 'UCS-4BE');
        }

        return chr($ascii);
    }

Более простая, но тоже не самая очевидная функция — addcslashes. Она экранирует указанные символы слешем, представляя все символы с кодом большим 128 как восьмеричное число, например, буква «ф» (в кодировке cp1251), если указать её во втором параметре, будет представлена «\364». Вопрос в следующем — символы в UTF-8 могу занимать больше одного символа, как их заэкранировать?

Ответ в парной функции — stripcslashes. Если последовательно заэкранировать все байты UTF-8-последовательности символа, то stripcslashes вернёт этот символ в первоначальное состояние. Код реализации я приводить не буду, слишком длинный. Что о нём нужно знать — последовательность экранируется только в том случае, если встречается целиком.

Моя задача — эмулировать поведение функции в точности, в этой связи str_pad содержит две загадки: если задана строка дополнения из двух символов и более, то как аргумент дополняется слева и какая сторона дополняется первой, когда дополнять надо обе стороны?

bolk@dev:~/daproject$ php -r 'var_dump(str_pad("..", 3, "12", STR_PAD_BOTH));'
string(3) "..1"
bolk@dev:~/daproject$ php -r 'var_dump(str_pad("..", 5, "12", STR_PAD_LEFT));'
string(3) "121.."

Как видим, сначала дополняется правая сторона, а при дополнении слева символы дополняют в указанном порядке (когда я программировал эту функцию, у меня был соблазн сделать дополнение с конца последовательности).

Вообще, в PHP функций с тонкостями поведения масса. Функция wordwrap (по-умолчанию) преобразовывает длинную строку в абзац заданной ширины, разрывая строку так, чтобы не разорвать слова. А вот какими символами, по мнению этой функции, разделяются слова? Мои эксперименты показывают, что это пробел и «\n». Интересно, что «\r\n» эта функция не разрывает! Так же нужно не забыть про то, что пробелы, вообще говоря, сохраняются, заменяется на символ переноса только необходимый минимум.

13 комментариев

PHP и UTF-8: четыре с половиной или некоторые функции

Я пока приостановил эпопею с UTF-8, так как сменились приоритеты, но обязательно ещё вернусь к этому вопросу. А пока, в паузах, потихоньку пишу для функций, для которых уже нет UTF-8-аналогов эти самые аналоги. Вот, например, очередной набор (это статические методы класса UTF, для краткости я убираю часть для работы с однобайтными строками):

public static function strcmp($str1, $str2) {
    // используется модуль intl
    return collator_compare(collator_create(''), $str1, $str2);
}

public static function strncmp($str1, $str2, $len) {
    return self::strcmp(self::substr($str1, 0, $len), self::substr($str2, 0, $len));
}

public static function strcasecmp($str1, $str2) {
    return self::strcmp(self::strtolower($str1), self::strtolower($str2));
}

static public function ctype_space($text) {
    return preg_match('/^\p{Zs}+$/us', $text);
}

Привожу в качестве иллюстрации того, что не всё так сложно как кажется. Стоит только сесть и начать.

Некоторые функции в PHP уже есть (mb_substr), какие-то требуют минимальных доделок (preg_replace), некоторые можно выразить через другие (trim через ltrim и rtrim), оставшиеся, конечно, придётся написать (strcmp, ltrim, addcslashes и так далее).

И, кстати, в ходе родился замечательный совет: если функцию трудно реализовать (например, str_ireplace), посмотрите, может проще переписать место, где она используется. У меня две функции были использованы ровно по одному разу.

Добавлено позднее: тщательно изучил вопрос сравнения Unicode, я выяснил, что производить сравнение так, как это сделал я нельзя, поэтому функцию strcmp я удалил, буду переписывать. Например, буква «ё» в Unicode стоит дальше «л», а должно быть наоборот.

Добавлено ещё позже: в PHP 5.3 появилось расширение Intl, основанное на известной библиотеке UCI, оказывается это расширение собирается и с PHP 5.2.4+, так что имеет смысл использовать именно его.

13 комментариев

IE9 beta

Internet Explorer 9 beta (39.95КиБ)

Вышла первая бета-версия IE9. Идея разместить табы и адресную строку на одной прямой явно неудачная.

Сайты открываются через пень-колоду, явно хуже, чем в последней platform preview, мой сайт выглядит, как какашидзе, даром, что показывался одинаково во всех браузерах, включая самые старинные IE Mobile. Зато ACID3 — 95%, одно слово — синтетика.

Тьфу.

14 комментариев

Пакетное скругление картинок v0̶.̶2̶.̶1 0.2.2

Ребята, качайте новую версию, я сделал antialiasing для углов. Ну и пришлось же помучиться! Я немного заболел, пока сидел дома, сделал.

Основная идея такова — рисуется красный круг на чёрном фоне, на него накладывается фильтр Гаусса (тут меня ждала засада — в PHP 4.4.4, который у меня, нет функций imagefilter и imageconvolution, зато последняя, написанная на чистом PHP есть в интернeтах).

Далее, я считаю красную компоненту круга альфа-каналом для указанного цвета фона и накладываю по точкам в четыре угла картинки четыре квадрата изображения с красным кругом. Формула пересчёта rgba → rgb очень простая.

Получилось, на мой взгляд, очень неплохо, но скорость обработки, конечно, снизилась.

Добавлено позже: обновил до 0.2.2 — теперь вся работа с временными файлами делается внутри временной папки (путь до неё берётся из переменных окружения).

25 комментариев

Пакетное скругление картинок v0.2

Новая версия пакетного скруглителя картинок. Из нового: короткие имена ключей (их видно после запуска), ключи для сохранения EXIF из оригинального файла (-​-save-exif), рекурсивного обхода папок (-​-recursive) и сохранения оригинальных имён файлов (-​-keep-name).

19 комментариев

PHP и UTF-8: четвёртый этап

В прошлой части мы кое-что заменили в коде, в этой части неплохо было бы поискать не забыли ли мы что-то ещё заменить. А это значит — куча работы руками.

Какие у нас есть варианты?
Какие у нас есть варианты? Функцию preg_replace_callback мы рассмотрели ещё в прошлый раз (а я это сделал сегодня, ничего криминального не нашёл), но есть и другие callback-функции, а так же eval, регулярные выражения с ключом «e» (PREG_REPLACE_EVAL), call_user_func, call_user_func_array и create_function.

Начнём с eval, его проще всего найти:

bolk@dev:~/daproject$ find -type f ( -name '*.php' -or -name '*.tpl' -or -name '*.inc' ) | xargs -n1 egrep -li 'bevals*('
./php/lib/magpierss/extlib/Snoopy.class.inc
./php/lib/PEAR.php
./php/dir1/file1.php
./php/dir1/file2.php
./php/dir1/file3.php

К счастью, почти всё что я нашёл — это eval-вызовы в коде на JavaScript внутри PHP-скриптов, так что править мне ничего не пришлось. В вызовах call_user_func, call_user_func_array и create_function я так же ничего криминального не нашёл (проверяются точно так же).

Теперь я попробую найти вообще все callback-функции. Это чуть сложнее сделать — в руководстве по PHP раздела, где они были бы перечислены нет. В chm-версии руководства я просто поискал строку «callback».

Не все callback-функции нас интересуют, нужны только те, которые возвращают строковый результат. Кроме того, нужно поискать использование констант, которые задают в некоторых вызовах callback-функции.

Вот что у меня получилось: readline_callback_handler_install, readline_callback_read_char, ob_start, session_set_save_handler, CURLOPT_HEADERFUNCTION, CURLOPT_PASSWDFUNCTION, CURLOPT_PROGRESSFUNCTION, CURLOPT_READFUNCTION, CURLOPT_WRITEFUNCTION, FILTER_CALLBACK, iterator_apply, set_exception_handler, pcntl_signal, array_map, array_filter, array_walk, array_walk_recursive, array_udiff_uassoc, array_diff_uassoc, array_udiff_assoc, array_diff_ukey, array_udiff , array_intersect_ukey, array_uintersect_uassoc, array_uintersect_assoc, array_intersect_uassoc, array_intersect_assoc , array_uintersect, array_reduce, uksort, usort, uasort.

Или для командной строки (регулярное выражение я тут подсократил):

bolk@dev:~/daproject$ find -type f ( -name '*.php' -or -name '*.tpl' -or -name '*.inc' ) |
> xargs -n1 egrep -H 'array_map|array_filter|array_walk|…|uasort' | less

В выводе нужно высматривать только вызовы строковых функций и статических методов, которые стоят на месте callback-параметров или переменные на тех же местах, в последнем случае нужно будет детально разобрать что находится в этих переменных. У меня на это ушло две минуты, это в проекте с 50000 строк кода.

Всё, что осталось найти — вызовы preg_replace с модификатором «e». Я, было, подумал как-то автоматизировать это дело, но просмотрел сколько у меня вхождений этой функции — оказалось около 200, быстрее просмотреть глазами. Мои старания были вознаграждены — я сделал несколько замен.

Думаю, в следующей части надо что-то сделать с явными указаниями кодировки. Впрочем, я ещё не решил.

2 комментария

Веб-сервис оптимизации SVG

Веб-сервис оптимизации SVG (11.36КиБ)

Евгений Рыжков изготовил из моего скрипта оптимизации SVG веб-сервис. Скрипт я когда-то писал для книги и он появился после существенного развития идей Джоша Хирта, который показал принципиальную возможность автоматической оптимизации этого формата.

6 комментариев

PHP и UTF-8: третий этап (давайте уже что-нибудь заменим)

Ну что ж, всё готово для того, чтобы наконец-то что-то заменить в нашем проекте. С прошлого этапа у нас должен появиться поправленный руками класс Utf, который нужно подключить так, чтобы он был доступен из любого файла нашего проекта. Например, для этой цели можно воспользоваться настройкой PHP «auto_prepend_file».

Что же дальше?
Следующая программа поможет нам заменить наши ущербные функции (их имена и статистика всё так же лежат в файле tool-func.log) на пока не менее ущербные обёртки.

<?
    if ($_SERVER['argc'] <> 3) die;
    if (realpath($_SERVER['argv'][2]) == realpath(__FILE__)) die; 

    $file = file_get_contents($_SERVER['argv'][2]);
    if (strpos($file, '<?') === false) die;

    $funcs = file($_SERVER['argv'][1], FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($funcs as &$func) {
        list(,$func) = preg_split('/\s+/s', $func, 2, PREG_SPLIT_NO_EMPTY);
    }

    $skip   = false;
    $skipIf = array(T_PAAMAYIM_NEKUDOTAYIM, T_DOUBLE_COLON, T_OBJECT_OPERATOR);
    $fixed  = false;

    ob_start();

    foreach (token_get_all($file) as $token) {
        if (is_scalar($token)) {
            echo $token;
        } else {
            if (!$skip && $token[0] == T_STRING && in_array(strtolower($token[1]), $funcs)) {
                $fixed = print 'Utf::' . $token[1];
            } else {
                echo $token[1];
            }

            $skip = in_array($token[0], $skipIf); 
        }
    }

    file_put_contents($_SERVER['argv'][2], ob_get_clean());

    if ($fixed) {
        echo $_SERVER['argv'][2], "\n";
    }

Запустить её можно, например, так (напоминаю, что у меня PHP находится в файлах с расширениями php, tpl и inc):

bolk@dev:~/daproject$ find -type f \( -name '*.php' -or -name '*.tpl' -or -name '*.inc' \) | grep -vF '.svn' |
> xargs -n1 php tool4.php tool-func.log
./php/file.php
./php/dir1/file1.php
./php/dir1/file2.php
./php/dir1/dir2/file3.php
…

Программа напечатает имена файлов, где произошла замена. Своеобразная проверка — запустите её ещё раз, на втором проходе никаких имён файлов выведено быть не должно! Обязательно проследите, чтобы запуск не затронул ваш класс Utf!

Итак, мы заменили функции на вызовы класса Utf. Всё работает? У меня не заработало. Причины две — я ошибся в имени одной из функций в классе Utf и появились проблемы с preg_replace_callback. О последней проблеме поподробнее.

Пока preg_replace_callback была встроенной функцией всё было замечательно, как только она стала статическим методом класса, проблемы появились. Всё дело в указании в качестве callback метода статического класса. Во-первых, метод может оказаться private или protected, что мешает ему быть вызванным из другого, неродного класса, во-вторых, в качестве имени класса может быть указано «self», а «self» внутри класса Utf равно «Utf», что совсем не то, что ожидается.

Такие вещи придётся править вручную. Если нет возможности провести тестирование, то можно воспользоваться подсказками IDE — во многих случаях она укажет на такие ситуации, но, в любом случае, лучше все такие места просмотреть глазами.

Сегодня ночью я улетаю в Екатеринбург, так что, думаю, это последняя часть на этой неделе.

Кстати, остаются ещё строковые функции, вызываемые как callback, внутри eval или в eval-выражениях preg_replace. Что-то с ними надо делать.

5 комментариев

PHP и UTF-8: третий этап (создание прототипов)

Этот сериал хорош тем, что я пишу его в реальном времени. Единственно, первая фаза — когда я собирал первоначальные данные и обдумывал как мне подступиться к этой задаче, заняла куда больше времени, чем может показаться. Итак, в предыдущей части мы выяснили какие модули используются в нашем проекте и узнали, что в нём кое-где указана кодировка прямым текстом. Так же держим в голове, что нам надо что-то будет делать с файловыми функциями и обращением к символу в строке по индексу ($str[$index]).

В этой части мы подготовимся к тому, чтобы заменить в проекте все упоминания небезопасных (с точки зрения работы с UTF-8) функций на статические методы класса Utf, где мы будем менять их на UTF-8-аналоги.

Что же дальше?
Вообще-то, я мог бы, конечно, вручную описать все 38 функций моего проекта, но мы же умеем программировать! Почему бы этим не воспользоваться? PHP даёт широкие (но, к сожалению, глючные) средства для исследования функций — это специальные классы расширения Reflection, которыми мы и воспользуемся.

<?
function U_makeproto($funcname)
{
    $func = new ReflectionFunction($funcname);

    $out = '    static public function ' . $funcname . '(';

    $counter = 0;
    $hasRefOptional = $hasOptional = false;
    $names = array();

    foreach ($func->getParameters() as $param) {
        $name = $param->getName();

        if ($name !== null && $name != '...') {
            $names[] = $name = '$' . $name;
        } else {
            $names[] = $name = '$param' . $counter++;
        }

        if ($param->isPassedByReference()) {
            $name = '&' . $name;
        }

        if ($param->isArray()) {
            $name = 'array ' . $name;
        }

        if ($param->isOptional()) {
            $name .= '=';
            $name .= $param->isDefaultValueAvailable() ? $param->getDefaultValue() : 'null';

            if ($param->isPassedByReference()) {
                $hasRefOptional = true;
            }

            $hasOptional = true;
        }

        $out .= $name . ', ';
    }

    $out = rtrim($out, ', ') . ') {';
    if ($counter || !$names) {
        $out .= "\n        // FIXME: Invalid proto ( https://www.php.net//$funcname )\n";
    }

    $out .= "\n        if (self::ON) {".
            "\n            // TODO: UTF version".
            "\n        }\n";

    if (!$hasRefOptional) {
        if ($hasOptional || !$counter && !$names) {
            $out .= "\n        " . '$params = func_get_args();'.
                    "\n        return call_user_func_array('$funcname', \$params);";
        } else {
            $hereNames = implode(', ', $names);

            $out .= "\n        return $funcname($hereNames);";
        }
    } else {
        $reqCounter = $func->getNumberOfRequiredParameters();
        $out .= "\n        switch (func_num_args()) {".
                "\n            case $reqCounter:".
                "\n                " . '$params = func_get_args();'.
                "\n                return call_user_func_array('$funcname', \$params);";

                for ($i = $reqCounter+1, $len = $func->getNumberOfParameters(); $i<=$len; $i++) {
                    $hereNames = implode(', ', array_slice($names, 0, $i));

                    $out .= "\n            case $i:".
                            "\n                return $funcname($hereNames);";
                }

                $out .= "\n            default:".
                        "\n                 trigger_error('Wrong parameter count for $funcname()', E_USER_WARNING);".
                        "\n                 return null;";

                $out .= "\n        }";
    }

    return $out . "\n    }";
}


echo "<?php\nclass UTF\n{\n    const ON = false;\n\n";
foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $funcname) {
    echo U_makeproto($funcname), "\n\n";
}

echo "}";

Как я уже сказал, расширение Reflection работает в PHP с багами, именно поэтому я максимально подробно стараюсь обработать ошибочные ситуации. После запуска программы, можно подсчитать, какое количество функций (у меня их 10) не отобразилось правильно, ничего не поделаешь, придётся их править руками. Файл tool-func.log содержит частоты появления функций с их именами из первого этапа эпопеи.

bolk@dev:~/daproject$ sed -re 's/\s+\S+\s+//' tool-func.log | php tool-func-proto.php > utf.php
bolk@dev:~/daproject$ grep -c FIXME utf.php
10

Я удаляюсь править прототипы руками, а в следующей части уже перейдём к непосредственной замене.

P.S. В коде есть странное место — «blah» . ’\$var’, увы, у меня какие-то глюки с форматированием на сайте, пришлось сделать так.

Оставить комментарий

← 1 45 46 47 48 49 67 →