вторник, 17 сентября 2013 г.

Переезжаю с Blogger на Logdown / Migrating from Blogger to Logdown

Ну что ж, пора мучений с HTML-разметкой закончилась. Нашёл блогоплатформу Logdown, там можно писать в github-flavored markdown, плюс подсветка кода встроенная. Плюс встроенные шаблоны просто чудесные, хотя шрифтам явно не хватает поддержки кириллицы.

В этом блоге больше писать не буду, буду в logdown теперь. Все старые посты отсюда перенёс туда (разметка, конечно, вся поехала, придётся чинить). Правда, этот блог удалять не буду, мало ли у кого постоянные ссылки сюда есть.

Новый адрес тут: hijarian.logdown.com (yeah, this is my new address, my dear English-speaking readers).

суббота, 20 июля 2013 г.

How to package and use Yii framework in PHAR archive

Okay, today's the task: pushing all of 1892 files of Yii framework to the Git repository is a burden, and upgrading it to new version pollutes the git log with changes to files you don't care about. Let's package it into a PHAR!

Packaging

Rasmus Schultz made a special script to package the Yii framework into a PHAR archive. I forked it to save for a future (at least my GitHub account will live as long as this blog).

You need just to put this script to the root of Yii codebase (cloned github repo, for example), and run it as usual:

php yii-phar.php

This will create the PHAR archive in the same directory.

Hovewer, beware the catch 1: PHP can refuse to create packed archive, emitting the following error:

PHP Fatal error:  Uncaught exception 'BadMethodCallException' 
with message 'unable to create temporary file' 
in /path/to/your/yii/root/yii-phar.php:142
Stack trace:
#0 /path/to/your/yii/root/yii-phar.php(142): Phar->compressFiles(4096)
#1 {main}
  thrown in /parh/to/your/yii/root/yii-phar.php on line 142

I decided to just remove lines 140 and 142 from the script:

echo "Compressing files ...\n\n";

$phar->compressFiles($mode);

And that's all. I can bear with 20 MB file in repo, and don't really care about compression.

Using

To connect the resulting PHAR to your Yii application, replace your usual:

require_once('/path/to/your/yii/framework/yii.php');

With the following:

new Phar('/path/to/yii.phar');
require_once('phar://yii/yii.php');

Note that in new Phar() invocation you should use real path to your phar archive file, but second line should be written verbatim, as the PHAR which becomes created is being made with alias 'yii', using feature described in the documentation for Phar::__construct.

However, of course, there's a catch 2: Yii built-in asset manager (CAssetManager) has too specific `publish` method, unable to cope with custom PHP streams. So, we need the fixed version.

I decided to create a descendant of `CAssetManager` descriptively called `PharCompatibleAssetManager` with the following definition exactly:

/**
 * Class PharCompatibleAssetManager
 *
 * As we use Yii packaged into .phar archive, we need to make changes into the Asset Manager,
 * according to the https://code.google.com/p/yii/issues/detail?id=3104
 *
 * Details about packaging Yii into .phar archive can be found at
 * https://gist.github.com/mindplay-dk/1607318
 */
class PharCompatibleAssetManager extends CAssetManager
{
  protected $_published = array();

  public function publish($path,$hashByName=false,$level=-1,$forceCopy=null)
  {
    if($forceCopy===null)
      $forceCopy=$this->forceCopy;
    if($forceCopy && $this->linkAssets)
      throw new CException(Yii::t('yii','The "forceCopy" and "linkAssets" cannot be both true.'));
    if(isset($this->_published[$path]))
      return $this->_published[$path];

    $isPhar = strncmp('phar://', $path, 7) === 0;
    $src = $isPhar ? $path : realpath($path);

    if ($isPhar && $this->linkAssets)
    {
      throw new CException(
        Yii::t(
          'yii',
          'The asset "{asset}" cannot be published using symlink, because the file resides in a phar.',
          array('{asset}' => $path)
        )
      );
    }

    if ($src !== false || $isPhar)
    {
      $dir=$this->generatePath($src,$hashByName);
      $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir;
      if(is_file($src))
      {
        $fileName=basename($src);
        $dstFile=$dstDir.DIRECTORY_SEPARATOR.$fileName;

        if(!is_dir($dstDir))
        {
          mkdir($dstDir,$this->newDirMode,true);
          @chmod($dstDir,$this->newDirMode);
        }

        if($this->linkAssets && !is_file($dstFile)) symlink($src,$dstFile);
        elseif(@filemtime($dstFile)<@filemtime($src))
        {
          copy($src,$dstFile);
          @chmod($dstFile,$this->newFileMode);
        }

        return $this->_published[$path]=$this->getBaseUrl()."/$dir/$fileName";
      }
      elseif(is_dir($src))
      {
        if($this->linkAssets && !is_dir($dstDir))
        {
          symlink($src,$dstDir);
        }
        elseif(!is_dir($dstDir) || $forceCopy)
        {
          CFileHelper::copyDirectory($src,$dstDir,array(
            'exclude'=>$this->excludeFiles,
            'level'=>$level,
            'newDirMode'=>$this->newDirMode,
            'newFileMode'=>$this->newFileMode,
          ));
        }

        return $this->_published[$path]=$this->getBaseUrl().'/'.$dir;
      }
    }
    throw new CException(Yii::t('yii','The asset "{asset}" to be published does not exist.',
      array('{asset}'=>$path)));
  }
}

I'm really, really sorry that you had to read this traditionally horrible Yii code, but that was inevitable... :(

Main change was starting from the $isPhar = strncmp('phar://', $path, 7) === 0; part.

Now just link this asset manager instead of built-in one:

config/main.php:
        'components' => array(
            'assetManager' => array(
                'class' => 'your.alias.path.to.PharCompatibleAssetManager',
            ),
        )

Congratulations!

Now your Yii web application uses phar archive instead of huge pile of separate files. They say that this increases performance, but my personal reasons was just to reduce the number of files inside the repository.

суббота, 29 июня 2013 г.

Running Database Dependent Tests on Ramdisk

Today's guess is this: you have a test harness which utilizes the database, and it has enough test cases in it for full test run to be so slow you cringe at the very thought of launching it.

We discuss a lifehack-style solution to this problem: putting the DBMS which will be used by our test harness completely on a RAM disk, so it'll operate from much faster memory than the hard drive (even faster than from SSD).

Main issue is this: as you probably need only a test datasets at ramdisk, and only for a period of running test suite, you will need separate DBMS instances to work on ramdisk, not the ones already installed on your system.

Here we'll look at how to prepare MySQL and MongoDB instances to work on ramdisk.

End Result

In the end you'll get the special preparation script which you should launch before your tests. After this, your test suite will run with the isolated MySQL and MongoDB instances on top of ramdisk.

If your test suite has large quantities of integration tests using databases, this will greatly increase the speed of test run. It is reported in one particular case that the drop in run time was from 1:30 to 18 seconds, 5 times faster.

Prequisites

You should have a *nix system as a MySQL Sandbox (see below) works only there. OSX probably will do, too. This system should have some bash-like shell (obviously) and Perl installed. Kernel should have tmpfs support. Your nonprivileged user should be able to mount filesystems, or you'll need to hack the script to introduce sudo at mount step (assuming your user can sudo).

For isolated MySQL instance you need the MySQL distirbutive downloaded from the website. For isolated MongoDB instance you need the MongoDB distirbutive downloaded from the website. Note, however, that the whole MongoDB server is contained in just a single binary file ~8MB in size.

Of course as we will work completely in memory you have to be sure that you have enough RAM to store your (presumably test) datasets.

Ramdisk

Making ramdisk is very simple with latest Linux:

mount -t tmpfs $RAMDISK_NAME $RAMDISK_DIR

as root.

RAMDISK_NAME is some identifier for mountpoints table. RAMDISK_DIR is the directory which will be turned into RAM-based filesystem.

After this mount action, anything you put into RAMDISK_DIR will be placed into memory, without interaction with the physical hard drive.

Of course, it means that after unmounting the ramdisk everything which was in it will be lost.

Shutting down the ramdisk

Just unmount the created mount point:

umount $RAMDISK_NAME

as root.

Note that you probably should stop all running services which still use the ramdisk prior to unmounting!

Isolated MySQL instance

We'll use the MySQL Sandbox project to launch isolated MySQL instances.

For it to work you need the MySQL distirbutive downloaded from the website.

MySQL Sandbox is installed with the following command:

# cpan MySQL::Sandbox

as root, and you'll need to run it as follows:

SANDBOX_HOME="$RAMDISK_DIR" make_sandbox "$MYSQL_PACKAGE" -- \
  --sandbox_port="$MYSQL_PORT_DESIRED" --sandbox_directory="$MYSQL_DIRNAME"

It's a one-liner split to two lines for readability.

Note that you need root privileges only to install the MySQL Sandbox application itself, all further communication with it will be done from unprivileged account, most possibly the same under which you launch the test suite.

We need to set the SANDBOX_HOME variable prior to launching the sandbox factory because that's how we control where it'll put the sandboxed MySQL instance. By default it'll use $HOME/sandboxes, which is probably not what you need. Note that RAMDISK_DIR is the same directory that the one we prepared in previous step.

MYSQL_PACKAGE is a full path to the MySQL distirbutive package downloaded from website. Please note that MySQL Sandbox will unpack it to the same directory and will essentially use this unpacked contents to launch the sandboxed MySQL. So, probably, you'll need to move the package to ramdisk, too, to increase performance of actually launching and running the MySQL server itself, however, note that unpacked 5.6.0 contents are 1GB in size.

Remember the MYSQL_PORT_DESIRED value you use here, because you'll need to use it to configure your test suite to point at correct MySQL instance.

MYSQL_DIRNAME is of least importance here, because it's just a name of a subfolder under the SANDBOX_HOME in which this particular sandbox will be put.

After make_sandbox ended it's routine you can check that your sandbox is indeed working by running:

"$RAMDISK_DIR/$MYSQL_DIRNAME/use"

Connection to Isolated MySQL Instance

You should use the following credentials to connect to sandboxed MySQL:

* host     : '127.0.0.1'
* port     : $MYSQL_PORT_DESIRED
* username : 'msandbox'
* password : 'msandbox'

Please note that you must use 127.0.0.1 value for host and not a localhost as usual, because of sandbox internal security configuration.

Shutting Down the Isolated MySQL Instance

To shutdown the sandboxed MySQL, issue the following command:

"$RAMDISK_DIR/$MYSQL_DIRNAME/stop"

or more forceful

"$RAMDISK_DIR/$MYSQL_DIRNAME/send_kill"

This commands are needed mostly to stop the working daemon; after the unmounting of ramdisk all of sandbox data will be purged out of existence.

Isolated MongoDB instance

MongoDB server is contained in just a single binary file so it'll be a lot more easier compared to MySQL.

You'll need the MongoDB distirbutive downloaded from the website, too. This time unpack it to some directory.

After that, you can launch a separate instance of MongoDB with the following command:

"$MONGODB_BIN" --dbpath="$MONGODB_DIR" \
  --pidfilepath="$MONGODB_DIR/mongodb.pid \
  --port $MONGODB_PORT_DESIRED \
  --fork --logpath="$MONGODB_DIR/mongodb.log"

MONGODB_BIN is a /bin/mongod path preceded by the full path to the unpacked MongoDB distributive. Here you can even use your system MongoDB package, in case you have it installed. As a full example, MONGODB_BIN can have a value of ~/systems/mongodb-linux-x86_64-2.4.4/bin/mongod

MONGODB_DIR is a path to directory under RAMDISK_DIR to which this MongoDB instance should put it's files. For example, it can be just a $RAMDISK_DIR/mongo.

As with MySQL, MONGODB_PORT_DESIRED is a crucial parameter to specify the correct MongoDB instance to connect to. Remember it as you will need to set it up in your test suite.

Connecting to Isolated MongoDB Instance

By default MongoDB do not enforce any usernames or passwords so you need to just use the hostname and port parameters.

* host : 'localhost'
* port : $MONGODB_PORT_DESIRED

For example, for PHP Mongo extension, you get a connection to this instance as follows:

$connection = new MongoClient("mongodb://localhost:$MONGODB_PORT_DESIRED");

Shutting Down the Isolated MongoDB Instance

As you provided the --pidfilepath commandline argument when launching the MongoDB server, the following command should do the trick:

cat "$MONGODB_DIR/mongodb.pid" | xargs kill ; rm "$MONGODB_DIR/mongob.pid"

Essentially we are feeding the kill command with the contents of pidfile and removing it afterwards.

Bash scripts to automate the sandbox starting and stopping

There is a GitHub repository with the example scripts nicely laid out along with the comments.

There are three scripts:

  • db_sandbox_properties.sh: this is the all variable parameters you need to properly setup the sandboxes.
  • db_sandbox_start.sh: this script you run before your test suite.

    It was updated with the command to copy the tables schema from source MySQL instance to sandbox MySQL instance, so, if you have an accessible up-to-date MySQL instance with the schema for your tests, this script will copy schema to sandbox so you will have a DB ready to accept test cases.

    NOTE that you will need the sudo rights for your unprivileged user to successfully mount the ramdisk. If you do not have them, you can hack the mount command in any way you see sufficient to successfully mount the ramdisk.

  • db_sandbox_stop.sh: this script you run when you don't need the sandbox anymore. It'll stop both MySQL and MongoDB and unmount the ramdisk (note that you'll need the sudo rights for this, too).

среда, 12 июня 2013 г.

Как запускать что-либо при запуске Debian

Допустим, хочется что-то запускать прямо при старте Debian. Вот необходимые действия для этого:

# vim /etc/init.d/myscript
# chmod +x /etc/init.d/myscript
# insserv myscript

В файле /etc/init.d/myscript должно быть по минимуму следующее:

#!/bin/sh

### BEGIN INIT INFO
# Provides:          myscript
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Run some stuff
# Description:       Some descriptions
### END INIT INFO

Подробности об этой шапке можно прочитать в вики Debian. Название скрипта myscript должно быть в точности повторено в названии файла скрипта, в поле Provides и при вызове insserv.

Ранлевелы можно посмотреть здесь: http://wiki.debian.org/RunLevel.

Про dependency-based-boot там же в вики написано.

четверг, 6 июня 2013 г.

Как написать веб-утилиту небольшого размера на Common Lisp

Когда-то написал небольшую программку для сообщества игры Prime World. Она парсит таблицу рейтингов и составляет по ней линейные графики, плюс вычислят средний рейтинг (в рейтинге указываются первые 25 мест) по каждому персонажу. Персонажей 62, соответственно, линий на графике тоже 62. :)

Здесь напишу, как и из чего сделал и как оно работает.

Полный исходный код выложил к себе на GitHub.

Как работает

План был такой: приложение представляет собой одну HTML страницу, с одним файлом CSS и одним Javascript.

При загрузке страницы яваскрипт сразу делает AJAX-запрос на бэкэнд за данными.

Бэкэнд грабит исходную HTML-страницу, при помощи XPath выбирает значения рейтингов, имена игроков, места и имена персонажей. Затем кодирует это всё в JSON и отправляет скрипту обратно.

Скрипт отрисовывает полученные от бэкэнда рейтинги в виде графика при помощи библиотеки Highcharts. Дополнительно бэкэнд возвращает вычисленные средние значения рейтинга; они отображаются в виде таблицы, которая обрабатывается javascript библиотекой DataTables, чтобы добавить сортировку.

Между таблицей и графиком переключаемся кнопками.

Вот само приложение.

Как я уже сказал, приложение достаточно маленькое.

В качестве недостатка можно упомянуть то, что каждый раз, когда приложение открывается, делается запрос на исходный сайт Нивала. Делать так, конечно, дурной тон, и стоит прикрутить какое-нибудь кэширование на стороне сервера. Хотя у них всё равно топ-25 не так часто обновляется, да и посещаемость никакая, так что для начала сойдёт.

Как собрано

Серверная часть основана на Hunchentoot, то есть, мы сами себе веб-сервер.

Вот все необходимые URL:

  1. /, по которому возвращается HTML страница приложения.
  2. /data, который AJAX endpoint, возвращающий JSON объект с данными
  3. /assets/scripts.js, клиентский скрипт, который раскладывает данные из JSON в график и таблицу
  4. /assets/styles.css, бумажка со стилями (с украшениями я не парился, в стилях только сокрытие некоторых элементов для инициализации интерфейса).
  5. /assets/loading.gif, показываем эту картинку пока /data грузится.
  6. /favicon.ico, иконка до кучи, честно стырил из пакета Html5 Boilerplate.

Хостимся на Heroku.

Извлечение исходных данных

Сам парсинг исходной веб-страницы полностью заключён в одном файле под названием dno.lisp, и единственная функция, которая оттуда нужна публично, это send-data-from-origin-as-json, которая генерирует строку в JSON формате, содержащую данные, нужные в scripts.js для того, чтобы построить графики и таблицу. Сам файл dno.lisp можно посмотреть в репозитарии на гитхабе, я здесь его приводить не буду, слишком большой.

В итоге мы можем делать (load "dno.lisp") и получать с этого функцию, которая даст нам JSON, который мы можем возвращать как результат AJAX запроса, то есть, то, что нужно для пункта 2.

Отдача статических файлов

В идеологии hunchentoot'а (если не углубляться в детали), инициализация приложения выглядит так:

  1. Настраиваем все пути в приложении, вызывая hunchentoot:create-folder-dispatcher-and-handler, hunchentoot-create-static-file-dispatcher-and-handler, hunchentoot:define-easy-handler и т. п. Это как раз было упрощено при помощи хелперов в предыдущем пункте.

  2. Создаём экземпляр приложения заклинанием:

    (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor 
                                      :port 4242)))
    

Для начала делаем три хелпера в отдельном файле, для того, чтобы добавление путей в Hunchentoot не было пыткой:

helpers.lisp

(defun publish-directory (uri dirname)
  (push (hunchentoot:create-folder-dispatcher-and-handler uri dirname)
        hunchentoot:*dispatch-table*))

(defun publish-file (uri filename)
  (push (hunchentoot:create-static-file-dispatcher-and-handler uri filename)
        hunchentoot:*dispatch-table*))

(defmacro publish-ajax-endpoint (uri name params &body body)
  `(hunchentoot:define-easy-handler (,name :uri ,uri) ,params 
     (setf (hunchentoot:content-type*) "application/json")
     ,@body))

Имея эти три хелпера, файл инициализации, фактически, будет иметь следующий вид:

init.lisp

;; Webroot
(publish-file "/" "webroot/index.html")

;; Favicon, just because
(publish-file "/favicon.ico" "webroot/favicon.ico")

;; Images, CSS and JS files referenced from index.html
(publish-directory "/assets/" "assets/")

;; AJAX endpoint to grab data to display in chart
(publish-ajax-endpoint "/data" data () (send-data-from-origin-as-json))

(defun run ()
  "Launch Hunchentoot web server instance on default port"
  (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242)))

Само содержимое файлов index.html, scripts.js и styles.css можно посмотреть на гитхабе, здесь я описываю только то, как приложение собрано, а не как оно работает.

Мы не можем сделать (publish-directory "/" "webroot/"), потому что мы хотим дефолтный хэндлер для пути /.

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

Теперь мы можем делать так:

(load "dno.lisp")
(load "helpers.lisp")
(load "init.lisp")
(run)

После чего открываем браузер по адресу http://localhost:4242/ и наблюдаем готовое приложение, при условии, что в том же каталоге, что и эти два скрипта, находятся папки webroot и assets с соответствующими файлами в них.

Структурирование

Теперь запакуем всё в ASD. Вот моё определение приложения:

dno.asd

(asdf:defsystem #:dno
    :serial t
    :description "Presents ratings of players of Prime World as charts."
    :author "Mark Safronov <hijarian@gmail.com>"
    :license "Public Domain"
    :depends-on (#:drakma
                 #:cl-ppcre
                 #:cl-libxml2
                 #:iterate
                 #:cl-json
                 #:hunchentoot)
    :components ((:file "package")
                 (:module :src
                          :serial t
                          :components ((:file "helpers")
                                       (:file "dno")
                                       (:file "init")))))

package.lisp

(defpackage #:dno
  (:export :run)
  (:use #:cl
        #:cl-user 
        #:hunchentoot))

Как видно, все три исходника на Common Lisp, описанные выше, были переложены в отдельный подкаталог src. Понятное дело, во всех файлах теперь первой строчкой идёт вызов (in-package :dno).

Заметьте, что в ASD прописаны библиотеки, от которых зависит приложение (cl-ppcre, cl-libxml2, iterate и cl-json используются только в dno.lisp, на самом деле), однако сам пакет (package.lisp) не подключает эти библиотеки. Это просто дело вкуса: я предпочитаю использовать символы из чужих пакетов по их полному имени, не сокращая. Так сразу понятно, в какую документацию лезть.

Теперь у нас такая структура файлов:

assets/
    scripts.js
    styles.css
webroot/
    index.html
    favicon.ico
src/
    dno.lisp
    init.lisp
    helpers.lisp
dno.asd
package.lisp

Запуск на локальной машине

Так как у нас теперь определён ASD пакет, мы можем воспользоваться механизмами Quicklisp для того, чтобы максимально просто загружать приложение на локальной машине.

Если в рабочем каталоге Quicklisp в подкаталоге local-projects сделать символическую ссылку на каталог приложения, то можно будет подключать пакет приложения простым вызовом (ql:quickload :dno).

Таким образом, кладём в корневой каталог приложения следующий скрипт запуска:

runner.lisp

(in-package :cl-user)
(ql:quickload :dno)
(dno:run)

Теперь видно, зачем нужна отдельная функция run, определённая в init.lisp: для того, чтобы разделить загрузку самого приложения в рантайм и запуск приложения как веб-сервера.

Всё, готовое веб-приложение можно запустить из консоли, например, так:

$ cd path/to/dno/app
$ sbcl
* (load "runner.lisp")

Понятное дело, вместо SBCL может быть любой Лисп на ваш выбор.

Консоль становится неюзабельной после этого. Когда понадобится загасить приложение — нажатие Ctrl+D убивает приложение вместе с рантаймом лиспа.

Деплой на Heroku

Благодаря специальному buildpack'у для CL появилась возможность хостить SBCL + Hunchentoot приложения на Heroku.

Для этого нужно подготовить приложение следующим образом:

  1. Настраиваем у себя подключение к Heroku.
  2. heroku create -s cedar --buildpack http://github.com/jsmpereira/heroku-buildpack-cl.git. Запоминаем название проги, которое Heroku нам сгенерировало.
  3. heroku labs:enable user-env-compile -a myapp, вместо myapp пишем название проги из п.2.
  4. heroku config:add CL_IMPL=sbcl
  5. heroku config:add CL_WEBSERVER=hunchentoot
  6. heroku config:add LANG=en_US.UTF-8

Теперь в корневом каталоге приложения нужно добавить следующий скрипт, который ожидает buildpack:

heroku-setup.lisp

(in-package :cl-user)
(load (merge-pathnames *build-dir* "dno.asd"))
(ql:quickload :dno)

Крайне важно то, что этот скрипт не выполняет (dno:run), как это делает runner.lisp, потому что buildpack сам запустит Hunchentoot, так что от init.lisp требуется только настроить пути.

После того, как этот скрипт добавлен в репозитарий (имя heroku-setup.lisp имеет значение), можно пушить: git push heroku master, при условии, что п. 1 выполнен полностью.

Поздравляю

Всё, теперь прога готова, есть как возможность запустить на локальной машине, так и деплой сразу в Сеть (доменное имя тоже вполне приличное получается).

По тому же принципу можно строить любые другие простые веб-приложения, добавляя в init.lisp определения для путей в приложении.

Конечно же, если прога сложная, то придётся делать какой-то кустарный роутер, а также скорее всего, не отдавать статичные HTML файлы, а генерировать страницы при помощи какого-нибудь шаблонизатора типа CL-WHO, CL-EMB или чего-то подобного. Яваскрипт и CSS тоже можно генерировать прямо из Common Lisp, для этого есть проекты Parenscript и css-lite соответственно.

Мне всё это не понадобилось, приложение достаточно простое чтобы сразу отдавать статичные ассеты и HTML.

среда, 16 января 2013 г.

Относительные пути в LaTeX

Документ, разбитый на несколько файлов

Допустим, у вас верстается книга достаточно большого объёма и вы структурировали её двумя уровнями иерархии. Главный файл лежит в корневом каталоге и называется videogamebook2.tex. Он вызывает глобальные макросы типа \documentclass и затем подключает главы следующим образом:

...

\input{preface}

\input{moo2/chapter}

\input{planescape/chapter}

...

То есть, каждая отдельная "глава" книги лежит в отдельной папке, и её главный файл называется chapter.tex.

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

...

\input{moo2/section-01/section}

\input{moo2/section-02/section}

...

Видно, что разделы также лежат каждый в своей подпапке и файлы называются просто section.tex.

Заметьте, что пути всё ещё начинаются от корневого каталога проекта (!).

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

Иллюстрации, подключаемые во вложенных файлах

Усложним задачу: допустим, мы хотим иллюстрировать наш текст, используя стандартный пакет graphicx. Иллюстрации, соответствующие разделу, у нас лежат в подкаталоге этого раздела, рядом с файлом section.tex.

Тогда в заголовке section.tex можно написать, что:

\graphicspath{{./moo2/section-01/}}

а в тексте писать просто

\includegraphics{start-galaxy.jpg}

То есть, путь надо писать опять-таки от корня, и обязательно завершать слешем, иначе LaTeX вас не поймёт.

вторник, 15 января 2013 г.

Минимальный шаблон книги в LaTeX

Вот как выглядит совершенно минимальный шаблон для книги, использующий только стандартные классы, ничего сверхъестественного:


\documentclass{book}

% Кодировка XXI века
\usepackage[utf8]{inputenc}

% Если книга на русском, то включаем эти три строчки
\usepackage[russian]{babel}
\usepackage{indentfirst}
\usepackage{misccorr}

% Включаем какой-нибудь другой шрифт, а не стандартный
\usepackage{dejavu}

% Колонтитулы с названием главы и нумерация
\usepackage{fancyhdr}
\pagestyle{fancy}

% Если хочется, чтобы в оглавлении были только главы, и ничего ниже
\setcounter{secnumdepth}{-1} 

% Если есть картинки
\usepackage{graphicx}
\DeclareGraphicsExtensions{.jpg,.png,.gif}

% Если хочется гиперссылок в книге 
% (в макете для печатного издания они бессмысленны, конечно).
\usepackage{hyperref}


\begin{document}

\title{A Book Title}

\maketitle

\tableofcontents

\include{preface}

\include{chapter-1}

\include{chapter-2}

% ...

\include{chapter-n}

\end{document}

Пример конечного результата можно посмотреть в моей сборке A Video Game Book.

На обложке будет только title, по центру, и дата сборки, тоже по центру. Если надо другую обложку, то придётся настроить дополнительную страницу.

понедельник, 7 января 2013 г.

Как скачать сайт целиком в Debian используя только командную строку

Иногда одной веб-страницы мало. Надо скачать все остальные объекты, доступные из этой веб-страницы. Для этого понадобится один только wget.

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

wget -m -k -np -w 1 --random-wait -U "Mozilla" -e robots=off <URL>

Значения параметров:

  • -m Скачать всё, начиная с заданного URL (собственно то, что нужно).
  • -k Исправлять ссылки в скачанных HTML документах, чтобы ссылались не на Сеть, а друг на друга.
  • -np Качать только то, что ниже заданного URL или на его уровне (в частности, не качать ничего с других доменов).
  • -w 1 --random-wait Прикидываться обычным пользователем, делая паузы случайной длины минимум в 1 секунду между каждым скачиваемым файлом.
  • -U "Mozilla" Прикидываться Фаерфоксом (не очень настойчиво: никакой фаерфокс не использует такой User-Agent).
  • -e robots=off Вообще-то, не используйте этот параметр. Он заставляет wget игнорировать правила, описанные в robots.txt.

За инфу спасибо HydTechBlog.

воскресенье, 6 января 2013 г.

Прохождения игр как научно-фантастическая литература

Составил небольшой сборник из трёх прохождений игр: Вангеров, Freespace 2 и Star Trek: Starfleet Command. Эти прохождения настолько эпичны, что уже практически являются научно-фантастическими рассказами.

Я убеждён, что все три расссказа прекрасны, и заслуживают того, чтобы их читали, невзирая на происхождение. Иначе эти произведения просто сгниют в подшивках старых журналов.
Составил я в PDF, положил себе в Дропбокс.

Про HTML-версию подумаю, меня интересовала только пэдээфка, если честно.

В продолжение темы: на Let's Play Archive есть столь же чудесные рассказы-прохождения для Master of Orion 2, Planescape: Torment и Arcanum: Of Steamworks and Magick Obscura. Однако, они на английском языке, так что A Video Game Book 2 на языке берёз и осин будет недоступна. ;)