Как стать автором
Обновить

Пробуем Typst, альтернативу LaTeX

Уровень сложностиПростой
Время на прочтение20 мин
Количество просмотров8.1K

Вы могли слышать об Typst, современной альтернативе LaTeX, написанный на Rust, или не могли, ведь на Хабре я нашёл лишь несколько статей о нём. Некоторые до меня подчёркивали, что есть некоторые недостатки у первого по сравнению с последним.

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

Содержание

  1. Тулчейн

    1. Загрузка через winget

    2. Сборка из исходников

  2. Компиляция проекта

  3. Где писать код

  4. Пишем код

    1. Котики

    2. Статья с Хабра

    3. Тяжело в учении

  5. Итого

Тулчейн

Typst предоставляет несколько вариантов для работы с кодом: это можно делать онлайн через их онлайн-сервис или с помощью CLI-инструментов и редактора кода.

Онлайн-сервис представляет собой редактор кода c разными тарифами, один из который бесплатный, включающий довольно объёмный список функций. Также есть возможность коллаборативной работы, если над проектом работает несколько человек. Всё, что нужно сделать для использования онлайн-редактора — зарегистрироваться.

В свою очередь, CLI-инструменты можно установить несколькими способами:

  • Скачать предкомпилированные файлы с GitHub-репозитория разработчика, распаковать и прописать в переменные среды путь до бинарного файла.

  • Воспользоваться утилитой winget на Windows, brew в MacOS, а на Linux загрузить из репозитория пакетов.

  • Собрать из исходников.

Загрузка через winget

Для пользователей MacOS и Linux, наверное, нет смысла объяснять, как установить, а вот обыватели Windows, скорее всего, мало знакомы с менеджерами пакетов, поэтому объясняю.

Winget — один из менеджеров пакетов, которые позволяют искать, устанавливать, обновлять, удалять и настраивать приложения парой команд из командной строки.

Загрузить его можно из Microsoft Store или так же, как и Typst, загрузить скомпилированные файлы из GitHub-репозитория.

После установки winget нужно открыть командную строку либо PowerShell. PowerShell легко можно открыть, нажав на правую кнопку мыши по меню «Пуск» и в контекстном меню выбрать «Windows PowerShell». Открыв командую строку, прописываем команду:

winget install --id Typst.Typst

Сборка из исходников

Для начала нужно установить тулчейн Rust, а после прописать команду:

cargo install --locked typst-cli

Пакетный менеджер Cargo загрузит файлы программы, включая зависимости, из репозитория crates.io и запустит процесс компиляции. По завершении компиляции программа будет считаться установленной, и её можно будет использовать из терминала командой typst. Посмотреть версию установленной программы можно следующей командой:

typst --version

Компиляция проекта

Для того чтобы скомпилировать typst-файлы в pdf (также поддерживается экспорт в png и svg) с помощью cli-инструментов, необходимо открыть командную строку в папке проекта и прописать команду.

typst compile main.typ out.pdf

где main.typ — путь до точки входа в проект, то есть главный файл, куда подключаются все модули, out.pdf — путь до выходного pdf-файла. Получить дополнительную справку по командам Typst можно с помощью параметра --help.

Где писать код

Отлично, CLI-инструменты установили, но где писать код? В любом удобном редакторе кода, где имеются плагины для Typst. Забавно, но плагины Typst есть чуть ли не под каждый редактор кода, что я видел, даже свежий Zed.

Но могу порекомендовать Visual Studio Code и пару плагинов для него: Typst LSP и Typst Preview. Typst Preview рекомендую лишь потому, что он работает шустрее, чем встроенный в первом. Также можете посмотреть другие плагины под Typst в маркетплейсе плагинов или любые другие, которые понравятся.

Если решили пользоваться VS Code и плагином Typst LSP, то рекомендую перейти в настройки плагина и переключить Export Pdf на значение never, если не хотите захламить проект pdf-файлами. Иначе будет компилировать каждый файл проекта.

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

Тарифы онлайн-сервиса Typst, машинный перевод
Тарифы онлайн-сервиса Typst, машинный перевод

На странице репозитория Awesome Typst сможете найти ещё некоторые инструменты для работы с Typst, включая специально заточенные под него редакторы, плагины, CLI-инструменты и прочее.

Обещанные фичи от разработчиков
Обещанные фичи от разработчиков

Помимо прочего, разработчики обещают настольное приложение в неизвестном будущем. Полагаю, они далеко ходить не будут и просто запихнут свой сервис в какой-нибудь Electron.js или Tauri.


Пишем код

Я буду использовать следующую организацию проектов:

cats   <--- Корень проекта
  ├───out.pdf   <--- Скомпилированный файл
  └───src       <--- Папка с исходниками проекта, картинками и прочим
       └───main.typ

Для упрощения себе жизни также создам очень простой скрипт для компиляции проекта compile.ps1. Скрипт получает на вход имя папки проекта. Затем он ищет в рабочей директории все подкаталоги и проверяет, нет ли среди них совпадающих с заданным именем. Если совпадения найдены, то скрипт приступает к компиляции проекта.

param ([string]$name)

$projects = Get-ChildItem -Directory | Select-Object -ExpandProperty Name

if ($projects -contains $name) {
    typst compile "$name/src/main.typ" "$name/out.pdf"
} else {
    Write-Host "Project $name not found. Existing projects:`n$($projects -join "`n")"
}

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

Котики

Для примера мы начнём с чего-то простого... Как насчёт конспекта о котиках?

= Котики

== Знакомство с различными породами

_Котята_ – это маленькие пушистые создания, которые могут растопить сердце любого человека. Однако выбор идеального котенка может оказаться непростой задачей. В мире существует множество различных пород, каждая из которых имеет свои особенности характера и внешнего вида. Перед тем как принять окончательное решение, важно изучить все доступные варианты и определиться с тем, какой именно питомец подойдет вам лучше всего.

== Определение предпочтений

Прежде чем начать поиск котенка, необходимо определить свои личные предпочтения. Ответьте на вопросы: 
- Какого *возраста* вы хотите котенка? 
- Какой *окрас* шерсти вас интересует? 
- Какой *размер* животного подходит для вашего дома? 
- Какие черты *характера* вы цените больше всего (например, игривость или спокойствие)? 
Ответив на эти вопросы, вы сможете сузить круг поиска и сосредоточиться только на тех породах, которые соответствуют вашим требованиям.

== Исследование пород

После того как вы определились с предпочтениями, начните изучение различных пород. Обратите внимание на такие факторы, как _здоровье_, _темперамент_ и _уровень активности_ каждой породы. Некоторые породы могут быть более _активными_ и _игривыми_, в то время как другие предпочитают _спокойный_ образ жизни. Также учитывайте возрастную категорию котят – некоторые породы достигают зрелости быстрее других.

== Посещение приюта или питомника

Когда вы выбрали несколько подходящих пород, посетите приют или питомник для знакомства с котятами лично. Общайтесь с заводчиками и задавайте им вопросы о здоровье животных, их характере и условиях содержания. Помните, что выбор котенка – это серьезное решение, поэтому не спешите и тщательно изучайте все доступные варианты.

== Заключение

Выбор идеального котенка – это процесс, который требует времени и терпения. Но если вы определитесь со своими предпочтениями заранее и проведете достаточное количество исследований, то найдете именно того питомца, который станет настоящим другом для всей вашей семьи.

Тут не особо много отличий от привычного всем Markdown, за исключением того, что вместо # для обозначения заголовков используется =, что довольно неплохой плюс в кассу Typst, а если учитывать то, что в том же LaTeX для выделения текста жирным необходимо использовать такие монструозные команды как \textbf{Котята}, то это огромный плюс.

Кстати, некоторые могли заметить, что... кириллица работает из коробки, не нужно танцевать с бубнами. Наверное, поэтому и говорят, что LaTeX маленько морально устарел. Это отнесём также к плюсам.

Отлично, а что, если мы захотим добавить изображения с котиками? Для этого уже требуется использовать скриптинг Typst, предоставляющий в наши руки переменные, функции, циклы, условия и модули, ака библиотеки. Все эти возможности можно использовать через оператор #: #hello("world"), #let name = "world", #{ let name = "world"; hello(name) }.

Обращаю ваше внимание на отсутствие токена ; в последнем примере. Всё дело в том, что используется принцип неявного возврата, как это реализовано в Rust. Это также правда и для других блоков: условий, циклов и прочих.

Возвращаясь к нашим козлам котикам. Для начала я создам папку images по пути cats/src, в которую сохраню файл cat.png, содержащий изображение котика. Теперь в файле main.typ можем использовать функцию image.

= Котики

#image("images/cat.png")

== Знакомство с различными породами

_Котята_ – это маленькие пушистые создания, которые могут растопить сердце любого человека. Однако выбор идеального котенка может оказаться непростой задачей. В мире существует множество различных пород, каждая из которых имеет свои особенности характера и внешнего вида. Перед тем как принять окончательное решение, важно изучить все доступные варианты и определиться с тем, какой именно питомец подойдет вам лучше всего.

Отлично! Но я чувствую, что изображение слишком большое, поэтому его можно уменьшить, передав в функцию аргумент width следующим образом: #image("images/cat.png", width: 70%).

Мы использовали относительные единицы (relative units). Относительные они потому, что для их вычислений используются значения родителя, в данном случае страницы. Также можно использовать абсолютные единицы (absolute units) измерения: сантиметры (cm) и дюймы (in).

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

= Котики

#align(center)[#image("images/cat.png", width: 70%)]

== Знакомство с различными породами

Вроде бы, выглядит неплохо, но! Нет пределу совершенству. Думаю, шрифт слишком агрессивный для такого рода контента, поэтому предлагаю изменить его на... Arial. Почему бы и да?

Для этого в начале файла установим правило #set text(font: "Arial"). Оператор set устанавливает глобально для всех последующих после него объектов text свойство font. Точно так же данный оператор работает и для других свойств и объектов.

Разве стало не лучше? Да, шрифт не самый лучший, но нет этих острых засечек и текст выглядит уютней.


Статья с Хабра

Предлагаю взяться за что-нибудь посложнее. Для примера попробую перенести часть своей предыдущей статьи в формат Typst. Поэтому создам новый проект slint с той же структурой.

= Пишем калькулятор на Rust с GUI

Зачем еще один калькулятор? Да незачем, просто как тестовый проект для рассмотрения GUI-библиотеки.

Изначально я хотел попробовать такие крейты, как GPUI, Floem и Xilem, но первая, кажется, пока работает только под MacOS и Linux, вторая не позволяет установить иконку окну и кушает оперативы побольше Webview в Tauri, а до третьей я так и не добрался, узнав об Slint.

Об Slint есть всего несколько новостных постов на Хабре, поэтому, возможно, вам будет интересно посмотреть, что это такое.

== Об Slint

Slint - это GUI-фреймворк, который, по заявлениям разработчиков, выделяется своей компактностью и легкостью использования. Сравниваясь с конкурентами, разработчики Slint утверждают, что их рантайм занимает всего 300 кб в оперативной памяти, что делает его идеальным выбором для embedded-систем.

Фреймворк официально поддерживает Rust, C++ и Node.js, и предоставляет собственный DSL, который затем  компилируется в нативный код перечисленных языков и платформы. DSL представляет из себя некую смесь CSS и SwiftUI, что делает точку входа значительно ниже.

Разработчики проекта предоставляют #link("https://slint.dev/get-started#integrate-with-ides")[плагины] для популярных и не очень IDE и редакторов кода, который даёт нам щепотку intellisense и немного кривой превью, который не может нормально отработать свойства `default-font-*`. Рекомендую к этому накатить ещё #link("https://codeium.com/")[Codeium], у них есть бета-поддержка Slint DSL (хотя на чудо рассчитывать не нужно).

Slint распространяется под несколькими лицензиями.

#image("images/licenses.png")

== Создаём проект
Для тех, кто мало знаком с Rust, буду всё подробно расписывать (ну, установить-то инфраструктуру языка, наверное, и без меня сможете, да? Да, ведь?...). Поэтому... создаём проект! Это делается следующими командами:

```bash
cargo new
# или
cargo init
```
По сути, обе команды делают одно и то же, но для cargo new нужно обязательно указать имя новой директории. Но если для cargo init не указать название, она создаст проект в текущей директории.

#quote()[Если Вам не нужен локальный репозиторий, который создаётся при инициализации проекта, стоит использовать флаг `--vcs none`.]

В нашем случае проект будет носить название `calc-rs`. Воспользовавшись командой, получаем проект со следующей структурой:

#figure(
  image("images/project.png"),
  caption: "Структура проекта",
  supplement: none
)

Что нового мы можем здесь наблюдать: функции link, quote и figure, а также преформатированный код bash. Не вижу большого смысла останавливаться на первых двух, поэтому предлагаю перейти сразу к figure.

Можете заметить, что внутри figure используется image без оператора #. Важно понимать различия между использованием функций внутри блоков кода и интерполяцией блока кода в текст через оператор #. Также можете заметить, что функции используют блоки, состоящие из квадратных скобок. Через эти блоки передаётся текст для обработки функцией. Для промежуточного преобразования содержимого, доступ к нему можно получить через свойство body, но об этом позже.

Всю мощь figure мы раскроем позже, а пока просто указываем подпись и убираем приставку Figure 1 c помощью свойства supplement: none.

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

#let code-color = rgb("#F7F7F7")

#set page(width: 585pt, height: auto, margin: (x: 15pt, y: 12pt+32pt))
#set text(size: 12pt, fill: rgb("#333"), font: "Microsoft Sans Serif")

#show figure: set text(size: 9.75pt, fill: rgb("#909090"))
#show par: set block(above: 18pt)
#show heading: it => {
  set block(above: 42pt)
  set text(
    font: "Fira Sans",
    weight: 500,
    size: 18pt - ((it.level - 1) * 3pt)
  )
  it
}

#show raw.where(
  block: false
): box.with(
  fill: code-color,
  radius: 3pt,
  inset: (x: 4.5pt, y: 0pt),
  outset: (y: 2.25pt)
)
#show raw.where(
  block: true
): set block(
  width: 100%, 
  fill: code-color, 
  inset: 15pt,
  radius: 3pt
)

#show quote.where(block: true): it => {
  table(
    stroke: none,
    columns: (auto, auto),
    table.vline(stroke: 3pt + rgb("#548EAA")),
    pad(x: 15pt, it.body)
  )
}

= Пишем калькулятор на Rust с GUI

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

Если хотите указать отступ у первого блока на странице относительно от верха страницы, то нужно либо использовать функцию v(), либо указывать в свойствах страницы через свойство margin: (top: value). Если укажите отступ на блоке, у которого хотите видеть отступ, то он успешно проигнорируется.

Далее мы настраиваем шрифт текста и его цвет одноимённой функцией text. Функция text предоставляет множество возможностей, поэтому рекомендую почитать в документации о ней. Но так как нам не надо, чтобы все объекты имели одни и те же настройки шрифта, мы их отдельно переопределяем.

Если я ранее не упоминал, то par — это объект параграфа, позволяющий определять отступы, межстрочный интервал и прочее. Если захотите настроить такое через text, у вас вряд ли что-то получится, так как он даже не виде строки представляется, а в виде массива строк, из-за чего такие манипуляции... затруднительны.

Наверное, на этом моменте уже могло прийти осознание того, чем отличаются set и show. Если до сих пор непонятно, то set определяет глобальные значения свойства объектов, а show позволяет эксклюзивно определить эти значения для объектов, соответствующих селекторам. Помимо этого, show позволяет полностью поменять итоговый вид объекта.

Для этого в show используются стрелочные функции, в которые передаются объект, найденный согласно селекторам, как это делается с heading. В нашем случае мы получаем на вход нашей функции заголовки, устанавливаем им размеры шрифта согласно их уровню, который получаем из свойства level, вместе с отступами и возвращаем их. Таким образом мы избежали излишнего кода.

На счёт селекторов, они в Typst в настоящее время крайне слабые и особых преимуществ не предоставляют, так как для выборки поддерживается буквально несколько объектов: heading, figure и... всё.
Нет, вы можете настроить через show и другие объекты, но селекторы по типу heading.where(level: 1).after(par) у вас не получится использовать, из-за чего придётся прибегать к шаблонам по типу #let chapter(name, body) = { ... }.

Про блочный raw не вижу особого смысла говорить, там не сложнее, чем до этого было. А вот в неблочном raw используется неявная упаковка, оборачивание в объект box. Объект box схож своими свойствами на объект block за исключением того, что box обтекается текстом.

Если не хотите засорять файлы с текстом листингами, можете вынести их в отдельные файлы, а после использовать, например, так: #raw(read("code/super_puper_feature.js"), lang: "javascript")

Также хочу обратить ваше внимание на свойства outset и inset. Это аналоги margin и padding, соответственно. В чём разница между margin и padding, можете посмотреть тут. Но в нашем случае мы указываем inset по оси Y, равный нулю, а outset уже ставим нужное нам значение. Это делается для того, чтобы наш элемент не уплывал и был на baseline строки.

Фух, на последок остался стиль для блока цитат. Он так же, как и raw, может быть блочным и неблочным, поэтому также используется селектор по свойству block. Нам нужно полностью поменять вид этого блока, поэтому используем стрелочную функцию, которая позволит получить доступ к контенту блока. Тут мы определяем таблицу без линий столбцов-строк и двумя колонками. В первую колонку определяем линию и нашу цитату, обёрнутую в объект pad (сокращение от padding).

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

Чуть не забыл сказать про шрифты. У меня в системе не установлен шрифт Fira Sans, который использует Хабр для заголовков. Можно скачать этот шрифт и поместить в проект, после чего указать компилятору путь до него через параметр --font-path. Собственно, я так и сделал, поместив шрифты в папку fonts, которая находится над всеми проектами. Казалось бы, где это может быть полезно, однако я считаю, что это очень удобно, так как если нужно передать кому-то проект и чтобы человек не искал все эти шрифты, он просто использует папку со шрифтами.

Если так же, как я и пользуетесь VS Code для работы с Typst, то для Typst Preview можно настроить пути до папки со шрифтами Font Paths.


Тяжело в учении

У нас отлично получается, как мне кажется. Но давайте перейдём к чему-то, с чем сталкиваются многие... К документам! Конкретно реализуем шаблон согласно ГОСТ 7.32-2017. Поэтому создам новый проект study. Также возьму первый попавшийся диплом из Интернета, по примеру которого уже будем верстать.

Давайте познакомимся с возможностью шаблонизации и разделения кода на разные документы. Для работы с модулями есть две функции: import и include. import позволяет получить из файла лишь функции и переменные, а include компилирует файл и уже готовый результат вставляет в выходной файл, поэтому не позволяет получить доступ к функциям. Начнём с титульного листа, для которого создадим файл title.typ со следующим содержимым:

#let title(
  faculty,
  specialty,
  area,
  subject,
  student,
  chief,
  city: "Тольятти"
) = [
  #set align(center)
  #set block(above: 20pt)

  #let inline(body) = box(baseline: 12pt)[#body]
  #let undertitle(title, width: auto, body) = {
    layout(size => {
      stack(
        dir: ttb,
        body,
        block(inset: (y: 3pt), 
          line(length: 
            if width == auto { 
              measure(body).width 
            } else { width },
            stroke: .5pt
          ),
        ),
        text(size: 9pt)[(#title)]
      )
    })
  }

  #grid(
    rows: (1fr, auto),
    [
      #text(size: 12pt)[МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ] \
      федеральное государственное бюджетное образовательное учреждение \
      высшего образования \
      "Тольяттинский государственный университет"

      #undertitle("наименование института полностью")[Институт математики, физики и информационных технологий]

      Кафедра #inline[#undertitle("наименование")["#faculty"]]

      #undertitle("код и наименование направления подготовки, специальности", width: 100%)[#specialty]

      #undertitle("направленность (профиль) / специализация", width: 100%)["#area"]

      #block(above: 60pt)[
        *#upper()[Выпускная квалификационная работа \ (бакалаврская работа)]*
      ]

      #block(below: 30pt)[
        на тему: #underline()["#subject"]
      ]

      #table(
        row-gutter: 18pt,
        stroke: none,
        columns: (auto, 2fr, 1fr),
        align: (x,y) => if x == 0 { left } else { bottom },
        rows: 2,
        [Студент], [#undertitle("И.О. Фамилия", width: 100%)[#student]], [#undertitle("личная подпись", width: 100%)[]],
        [Руководитель], table.cell(colspan: 2)[ #undertitle("ученая степень, звание, И.О. Фамилия", width: 100%)[#chief] ]
      )
    ],
    [#city, #datetime.today().year()]
  )
]

Как можем наблюдать, у нас есть функция-шаблонизатор title, которая принимает на вход значение и подставляет их в шаблончик. Ну не круто же? Если кто-то задаётся вопросом, зачем всё обёрнуто элементом grid, то это необходимо для того, чтобы город и дата были внизу. Это также можно провернуть через table или stack.

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

Может, я не понял и делал что-то не так, но при попытке получить высоту блока возвращает высоту строки. Казалось бы, в чём проблема: получаешь высоту строки, количество строк и их произведение, но получить количество строк, если это не raw-объект, сложно, если не невозможно. Именно поэтому в примере со статьёй с Хабра использовалась таблица и vline. Подумал, в реализации vline найду ответ, но... vline захардкожена в rust-коде.

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

#set page(margin: (left: 3cm, right: 1cm, y: 2cm))
#set text(size: 14pt, font: "Times New Roman", lang: "ru")
#set par(justify: true)

#import "title.typ": title

#title(
  "Прикладная математика и информатика",
  "Прикладная информатика",
  "Бизнес-информатика",
  "Разработка веб–приложения для обработки заказов предприятия проката оборудования (на примере ООО «Айсберг»)",
  "М.Ю. Родькина",
  "Н.Н. Казаченок"
)

Отлично выглядит, даже лучше оригинала. Предлагаю двинуться дальше, но аннотацию, введение опущу и сделаю лишь часть первой главы, создав файл src/chapters/chapter1.typ. В main.typ буду уже использовать include вместо import, который использовали для титульного листа.

Я не буду приводить код главы, можете его посмотреть в репозитории, покажу лишь интересующие нас фичи. Перво-наперво, метки и ссылки:

Программа «Прокат–Эксперт» создана для автоматизации пунктов проката любых предметов и оборудования. Программа производит учет выдаваемых предметов, платежей и взаиморасчетов с клиентами и подготавливает к печати документы и отчеты. Интерфейс программы представлен на @общ-вид-прокат-эксперт. 

#figure(
  image("../images/Скриншот Общий вид программы проката «Прокат Эксперт».png"),
  caption: "Скриншот Общий вид программы проката «Прокат Эксперт»"
) <общ-вид-прокат-эксперт>

Для тех, кто не знаком с метками и ссылками, то это невероятно удобная вещь, которая позволяет автоматически ссылаться на нужное изображение, таблицу и прочее. Поэтому код выше превращается в следующее:

В Typst также есть возможность делать метки и на заголовки и на обычные блоки текста (но это не точно), которые можно использовать в ссылках.

Что касается стилизации figure, то это немного неудобно... Это совсем неудобно. Я не нашёл решения, как убрать центрирование подписей над таблицами, например, не переделывая полностью компонент figure.caption. Считаю, что в LaTeX это куда удобнее и проще, как бы странно это ни звучало.

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

#set par(first-line-indent: 1.25cm) # Отступы во всех параграфах, кроме первого
#show heading.where(level: 3): it => [#it #h(1.25cm)] # Переопределение заголовка
                                                      # с отступом после него

Чтобы настройки стилей не мешались в главном файле, я вынесу их в файл preambule.typ.

#let conf(doc) = {
  set par(first-line-indent: 1.25cm)
  set list(indent: 1.25cm)

  set heading(numbering: "1.")
  show heading.where(level: 1): it => [Глава #(counter(heading).display(it.numbering)) #it.body]
  show heading.where(level: 3): it => [#it #h(1.25cm)]

  show table: set text(size: 12pt)
  show table.cell.where(y: 0): strong

  show figure: it => [#it #h(1.25cm)] 
  show figure.where(
    kind: table
  ): set figure.caption(position: top)

  [#doc]
}

Наверное, вы хотите спросить, что за аргумент doc используется? Ответ кроется в main.typ. Если использовать стейтмент show без явного селектора, то будет выбираться абсолютно весь документ. То есть получается, что мы передаём в шаблон conf контент, который находится после стейтмента show и который шаблон уже рендерит с применением стилей.

#import "preambule.typ": conf
#import "title.typ": title

#title(
  "Прикладная математика и информатика",
  "Прикладная информатика",
  "Бизнес-информатика",
  "Разработка веб–приложения для обработки заказов предприятия проката оборудования (на примере ООО «Айсберг»)",
  "М.Ю. Родькина",
  "Н.Н. Казаченок"
)

#show: conf # то же самое, что show: doc => conf(doc)

#include "chapters/chapter1.typ"
#include "end.typ"

Нам нужно оглавление и списки используемых материалов. Для этого существует функция outline. Если явно не указывать тип объектов, то будет создавать оглавление.

#import "utils.typ": t
# ...
#show: conf

#t(outlined: false)[Оглавление]
#outline(title: none)

#pagebreak()

#include "chapters/chapter1.typ"
#include "end.typ"

В силу того, что были изменены стили заголовков, выдаваемый заголовок компонентом outline ломает проект. Поэтому пришлось отключить заголовок и написать свой компонент. В силу того, что если объявить его в файле main.typ и попытаться использовать в end.typ, то компилятор закономерно выбрасывает ошибку зацикленного импорта. Поэтому был создан файл utils.typ с описанием компонента t от «title».

#let t(outlined: true, body) = {
  align(center)[
    #block(below: 26pt)[
      #show heading: it => [
        #set text(size: 16pt)
        #it.body
      ]
      #heading(outlined: outlined)[
        #upper(body)
      ]
    ]
  ]
}

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

#import "utils.typ": t
#let list(title, kind) = [
  #pagebreak()
  #t()[#title]
  #outline(
    title: none,
    target: figure.where(kind: kind)
  )
]

#list("Список таблиц", table)
#list("Список изображений", image)

Что касается списка литературы, то для него используется функция bibliography, которая может работать с файлами типа .bib (BibLaTeX) и новый формат файлов Hayagriva .yml, поддерживаемый разработчиками Typst. Для примера состряпаем небольшой файлик, но рекомендую открыть документацию по формату, она там не особо большая.

harry:
    type: Book
    title: Harry Potter and the Order of the Phoenix
    author: Rowling, J. K.
    volume: 5
    page-total: 768
    date: 2003-06-21
    language: en-US

electronic:
    type: Web
    title: Ishkur's Guide to Electronic Music
    serial-number: v2.5
    author: Ishkur
    url: http://www.techno.org/electronic-music-guide/
    language: en-US

dostoevsky:
    type: Book
    title: Преступление и наказание
    author: Достоевский, Ф.М.
    page-total: 400
    date: 2003-06-21
    language: ru-RU

Немного изменим компонент list и используем функцию bibliography. Как можете заметить, есть даже стилизация по ГОСТ, но проблема в том, что это лишь стилизация, без сортировки по языку и типу источника. Поэтому придётся самому сортировку настраивать.

#import "utils.typ": t
#let list(title, kind, body: none) = [
  #pagebreak()
  #t()[#title]
  #if body == none {
    outline(
      title: none,
      target: figure.where(kind: kind)
    )
  } else {
    body
  }
]

#list("Список таблиц", table)
#list("Список изображений", image)

#list(
  "Список источников", none, 
  body: bibliography("books.yml", full: true, title: none, style: "gost-r-705-2008-numeric")
)

С использованием ChatGPT был сгенерирован простой питон-скрипт приоритетной сортировки YML-файла со списком литературы. Скрипт может работать не так, как вы ожидаете, ибо он буквально за минуту был сделан и не тестировался. Если используете иностранные источники, то рекомендую указывать параметр language в обязательном порядке для нормальной сортировки. Если кто знает нормальную реализацию, то, пожалуйста, пишите в комментарии.

import yaml
import argparse

def sort_key(item):
    value = item[1]
    # Priority for books
    type_priority = 0 if value.get('type', '') == 'Book' else 1
    # Priority for Russian language sources
    language_priority = 0 if value.get('language', '').startswith('ru') else 1
    return (type_priority, language_priority, value.get('author', ''), value.get('date', ''), value.get('language', ''))

def sort_yaml(in_file, out_file):
    # Read YAML data from the input file
    with open(in_file, 'r', encoding='utf-8') as file:
        data = yaml.safe_load(file)

    # Sort the data
    sorted_data = dict(sorted(data.items(), key=sort_key))

    # Generate sorted YAML content
    sorted_yaml_content = yaml.dump(sorted_data, allow_unicode=True, sort_keys=False, default_flow_style=False)

    # Save the sorted YAML content to the output file
    with open(out_file, 'w', encoding='utf-8') as file:
        file.write(sorted_yaml_content)

if __name__ == "__main__":
    # Command line arguments setup
    parser = argparse.ArgumentParser(description='Sort YAML file according to ГОСТ Р 7.0.5–2008')
    parser.add_argument('inFile', type=str, help='Input YAML file')
    parser.add_argument('outFile', type=str, help='Output YAML file')

    args = parser.parse_args()

    # Sort the YAML file
    sort_yaml(args.inFile, args.outFile)

Хотел реализовать в PowerShell, как и предыдущий скрипт, но почему-то ни один модуль для работы с YAML не захотел работать, плевавшись миллиардом ошибок по всевозможным причинам.

После этого в тексте можно использовать ссылки на источники двумя способами:

  1. С помощью ссылки по ключу. То есть источник у вас в .yml файле записан как the-best-book, то и ссылка будет с тем же именем @the-best-book.

  2. С помощью прямого вызова функции cite(label("the best book")). Используется, если название источника содержит символы, не поддерживающиеся в ссылках Typst.

По мнению экспертов, рынок проката в России сейчас переживает небывалый рост, а такая область как прокат строительного инструмента набирает с каждым днем всё большую популярность. Как известно, для ремонта, строительства, как и других работ оптимально применять наиболее качественное оборудование и инструменты. Это помогает значительно сэкономить время и силы. @dostoevsky

Очевидно, что приобрести для себя профессиональный инструмент может далеко не каждый, это, как правило требует значительных вложений и при этом, в подавляющем большинстве случаев, оборудование или инструмент требуются на короткий промежуток времени. Таким образом, приобретать его для себя совершенно экономически не выгодно. @electronic

Именно поэтому пункты проката различного оборудования для стройки, ремонта, дачных работ пользуются значительной популярностью, именно прокат может значительно уменьшить стоимость ремонтных, строительных или дачных работ. По оценкам экспертов товарного рынка, услугами подобных пунктов проката готово пользоваться около 10% жителей современных городов. @harry

На счёт формул в Typst рекомендую просто открыть документацию, там не особо много функций, легко запоминаются, но вот для примера:

$ "Формула" arrow(a) = frac(x^3, x-1) $

Можете заметить, что если код вставляется через #, то формулы пишутся между $$.

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

#import "@preview/cetz:0.2.2"

#align(center)[
  #cetz.canvas({
    import cetz.draw: *

    circle((90deg, 3), radius: 0, name: "content")
    circle((210deg, 3), radius: 0, name: "structure")
    circle((-30deg, 3), radius: 0, name: "form")

    for (c, a) in (
      ("content", "south"),
      ("structure", "north"),
      ("form", "north")
    ) {
      content(c, align(center, c + [\ oriented]), padding: .1, anchor: a)
    }

    stroke(gray + 1.2pt)
    line("content", "structure", "form", close: true)

    for (c, s, f, cont) in (
      (0.5, 0.1, 1, "Typst"),
      (1, 0, 0.4, "DVI"),
      (0.5, 0.5, 1, "PDF"),
      (0, 0.25, 1, "CSS"),
      (0.5, 1, 0, "XML"),
      (0.5, 1, 0.4, "HTML"),
      (1, 0.2, 0.8, "LaTeX"),
      (1, 0.6, 0.8, "TeX"),
      (0.8, 0.8, 1, "Word"),
      (1, 0.05, 0.05, "ASCII")
    ) {
      content(
        (bary: (
          content: c, 
          structure: s, 
          form: f
        )),
        cont,
        fill: rgb(50, 50, 255, 100),
        stroke: none,
        frame: "circle"
      )
    }
  })
]

Итого

Typst зарекомендовал себя как неплохая альтернатива LaTeX. Однако, насколько мы смогли понять, у него есть некоторые проблемы на текущем этапе развития, хотя проекту чуть больше года, если я правильно понял. Но даже если это так, он вполне жизнеспособен и пригоден для использования.

Я старался не вываливать сразу всё, а постепенно вводить новые фичи Typst на примерах. Где-то мог путаться в понятиях или вовсе показывать неправильные примеры, поэтому прошу писать об этом в личные сообщения, как и об ошибках.

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

Весь код, приведённый в статье, вы можете найти в репозитории. Не заявляю, что мои варианты — эталонные шаблоны, поэтому если есть что улучшить, обязательно пишите в комментарии!

Теги:
Хабы:
+35
Комментарии55

Публикации

Истории

Работа

Rust разработчик
8 вакансий

Ближайшие события

AdIndex City Conference 2024
Дата26 июня
Время09:30
Место
Москва
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область