• Программа Preview

    Если я когда-нибудь пересяду с Мака на что-либо другое, больше всего мне будет не хватать… программы Preview. Да, встроенной программы просмотра изображений. Все потому, что Preview — невероятно мощный софт. Просмотр картинок и PDF лишь малая часть ее возможностей. Ниже я расскажу, что именно она умеет.

    (1) Банально, но все-таки — Preview открывает картинки и PDF. Не нужно ставить сторонний софт, который пропишет себя в автозагрузку и службы. С тяжелым сердцем вспоминаю Adobe Reader, который разросся до размера офисного пакета.

    Поставив Windows 10 на игровой комп, был удивлен, что в ней нечем открыть PDF (браузер Edge не в счет). По клику на PDF открывается магазин Microsoft – как это возможно в 2022 году, не понимаю. А на Маке сел — и читаешь PDF.

    Read more →

  • Коротко об lndir

    Небольшая заметка о утилите, которая требуется редко, но метко – lndir.

    Когда у меня стало больше одного Мака, появилась проблема синхронизации настроек. Другими словами, чтобы всякие .thisrc и .thatrc были одинаковы и подхватывались при изменении. Сюда входит конфиг Emacs, ssh, словари aspell, профили AWS, конфиги ctags, zshell и многое другое.

    Легче всего держать dot-файлы в репозитории и ставить симлинки. Неожиданно я столкнулся с тем, что не так легко написать shell-скрипт, который бы поставил симлинки на файлы из папки. Пытался при помощи find ... -execute и xargs, но постоянно что-то мешало. То отсутствие файла, то его существование, словом, всякие досадные случаи. В итоге было две команды: сначала одна удаляет симлинки, а вторая создает их заново.

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

    Пример: в каталоге ~/work/System/Dotfiles хранятся оригинальные файлы Emacs, .ssh и прочие. Следующая команда make расставит симлинки в домашнюю папку:

    HOME = /Users/ivan
    PWD = $(shell pwd)
    
    create-symlinks:
    	lndir ${PWD}/Dotfiles ${HOME}
    	chmod 600 ${HOME}/.ssh/*
    

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

    Синхронизация происходит обычным способом через git. Как только вы поменяли один из файлов, делаете коммит и пуш. На другой машине пулл и make create-symlinks, и все подхватывается. Репозиторий, понятно, должен быть приватным.

    По умолчанию lndir нет в поставке Линукса и Мака. Ставится из привычных apt и brew.

  • Интерфейс Гитхаба

    У меня бомбит от интерфейса Гитхаба. Он работает по странному принципу: показывает то, что не нужно и не показывает то, что нужно.

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

    В интерфейсе PR файлы задвинуты на последнюю вкладку “Files changes”, и я не верю, что этому есть разумная причина. Файлы это суть PR, с какой стати задвигать их в конец? Это же самое нужное! Изменения должны быть сразу под описанием, чтобы не кликать, а просто смотать экран.

    Есть ли на этой вкладке хоть грамм полезной информации? Если да, чем он важнее 18 измененных файлов?

    Далее эти дурацкие табы. Проблема в том, что на вкладках располагаются не только данные, но и кнопки. Например, закрыть PR можно только со вкладки “Conversation”. А поставить аппрув только со вкладки “Files”. В итоге постоянно кликаешь на первый и последний табы.

    Первый и последний, Карл! Уже это говорит, что как минимум они должны быть рядом, если не объединены в один.

    По этой причине я пользуюсь трюком: когда кидаю ссылку на PR, добавляю к концу /files, то есть не ...project/pull/3, а ...project/pull/3/files. При таком раскладе у человека сразу откроются файлы, и не придется переключать табы. Мелочь, а приятно, особенно если собеседник понимает этикет и отвечает тем же.

    Судите сами: вот мне пришел PR с комментарием. Рассмотрим действия, которые я должен выполнить для мерджа:

    • открыть ссылку на PR;
    • перейти на files, чтобы бегло посмотреть, что внутри;
    • если все хорошо, вернуться на вкладку conversation;
    • нажать merge;
    • нажать delete branch.

    По мне все можно уместить на одной странице и работать без кликов.

    Из-за особенностей интерфейса кнопка “delete branch” находится выше “merge”, то есть сначала нажимаешь ту кнопку, что ниже, а потом выше. Это ни в какие ворота: экран мотают сверху вниз, и чем ниже кнопка, тем она важнее.

    В целом интерфейс Гитхаба шумный и грязный. На закладке “Conversation” схематично указаны коммиты, хотя есть отдельная вкладка “Commits”. Зачем размазывать их по двум вкладкам? Все, что касается коммитов, должно быть в “Commits”.

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

    igrishaev commented 20 minutes ago
    No description provided
    

    Спрашивается, что именно commented? Зачем писать о том, чего нет?

    Особую грусть вызывает мобильное приложение Гитхаба. Почему-то оно не может нормально показать код: кнопка “Browse code” вечно болтается внизу. Что мешает сделать ее первой? В этом плане Гитхаб напоминает современный Дропбокс: приложение плохо показывает то, с чем работает.

    Однажды я хотел закрыть PR со спамом, но не нашел кнопку “Close pull request”. Возможно, она была под выпадашкой или вроде того, но увы, я пас.

    Слева: где посмотреть код? Справа: как закрыть PR?

    Мне кажется, интерфейс Гитхаба разжирел, и пора устроить ему чистку. Он достиг стадии “впихнуть невпихуемое”, как это бывает с продуктами, которые часто выкатывают фичи. Интерфейс должен щадить пользователя: не вываливать на голову все подряд в надежде, что кому-то пригодится. Интерфейс — это оборона пользователя от того ада, что творится на серверах. Давайте помнить об этом, хоть я и не верю, что дизайнеры Гитхаба меня услышат.

  • Деджаваскриптизиция (4)

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

    Как только я убрал Disqus, полезли спамные комментарии. Каждый день приходят два-три предложения купить виагру, надувную лодку или просто левые ссылки. Поскольку каждый комментарий открывает PR в репозиторий, все остается в истории Гитхаба. Посмотреть на это добро можно по ссылке.

    Разгребать подобные комментарии нет желания, поэтому должна быть минимальная защита от спама. С условием — без Js. Надумал такую схему:

    • капча генерируется на этапе сборки блога. На выходе получается HTML-форма с полем captcha и значением 2 × 5.
    • В форму добавляется поле для решения.
    • Сервер парсит капчу, решает и сверяет с ответом. Если что-то не так, заворачивает комментарий.

    Как ни странно, даже на таком примитиве боты отваливаются. Разве что с оговоркой: когда был оператор +, боты решали капчу. Как только заменил на × (знак умножения в юникоде), стала тишь да благодать. Надеюсь, читатель не забыл таблицу умножения! Тестируя форму, сам подвис с примером 8 × 9.

    Техническая сторона: вот построить капчу в шаблоне:

    
    {% assign val1 = '1 2 3 4 5 6 7 8 9' | split: ' ' | sample %}
    {% assign val2 = '1 2 3 4 5 6 7 8 9' | split: ' ' | sample %}
    {% assign op   = '×'            | split: ' ' | sample %}
    {% assign captcha = val1 | append: " " | append: op | append: " " | append: val2 %}
    
    

    Замечу, что при каждой сборке блога значения будут разные.

    Скрытое поле в форме:

    
    <input required name="captcha" type="hidden" value="{{ captcha }}">
    
    

    Виджет для ввода решения:

    
    <div class="block">
        <span class="comment-form-label"><small>{{ captcha }} = </small></span>
        <input required id="comment-form-solution" name="solution" type="text">
    </div>
    
    

    Наконец, серверный код проверки капчи:

    (dеfn validate-captcha [captcha solution]
      (when-let [[_ val1-raw op-raw val2-raw]
                 (re-find #"^(-?\d+) (.+?) (-?\d+)$" captcha)]
    
        (let [val1
              (Integer/parseInt val1-raw)
    
              val2
              (Integer/parseInt val2-raw)
    
              op
              (case op-raw
                ("+" "&#43;") +
                ("*" "×" "&#215;") *
                nil)]
    
          (when (and val1 val2 op)
            (= (str (op val1 val2))
               (str/trim solution))))))
    

    Грубо, неуклюже, но работает.

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

  • Деджаваскриптизиция (3)

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

    Форма:

    Экран возврата:

    А что, мне нравится.

    Расскажу, как это работает. В прошлый раз я перенес комментарии Disqus в репозиторий. Каждый комментарий — это файл формата YAML + markdown. В числе прочего он хранит ссылку на пост. Когда я собираю блог, в подвал каждой заметки подставляются ее комментарии.

    Для приема комментариев я следую тому же принципу: чтобы они появились на сайте, нужно создать файл в репозитории. Теоретически любой может оформить pull request, но это сложно. Должен быть сервис, который преобразует ввод пользователя в pull request для блога. Этот сервис я написал и назвал blog-backend.

    Сервис напоминает веб-приложение: оно принимает HTTP-запрос с формой. После прелюдии с валидацией перехожу к главному: интеграции с Гитхабом. Это оказалось не так-то просто. У Гитхаба уже три рестовых API, но ни одно не покрывает все возможности. Кроме REST есть убер-апишка на GraphQL — она-то мне и нужна.

    Интеграция с GraphQL была нелегкой: это самобытный язык и вещь в себе. Если бы в текущем проекте не было GraphQL, я бы полез на стенку. Во-вторых, сложность процесса: чтобы сделать PR силами API, нужно четыре вызова:

    1. получить метаданные репозитория, в том числе последний коммит;
    2. создать ветку от этого коммита;
    3. сделать коммит в новую ветку;
    4. сделать PR.

    Как по мне, апишка не консистентна: где-то на репозиторий ссылаешься по имени, а где-то по машинному ID, который приходит из метаданных. То же самое с ветками: где-то имя, где-то айдишник. Ответы GraphQL развесистые, с глубокой вложенностью. Например, последний коммит ветки погружен на пять уровней:

    (-> resp-get-repo :data :repository :ref :target :oid)
    

    С такой апишкой невозможно программировать без образца данных.

    Когда подружился с Гитхабом, настала проблема хостинга: где и как размещать логику. Мне понравился опыт с облаком Яндекса, и я продолжил эксперименты. Сервис написан на Кложе, скомпилирован Граалем в бинарь и хостится в Яндекс.Функции — аналоге AWS Lambda.

    В свою очередь, Яндекс.Функция — это облачный сервис, где размещают какой-то код и дергают его по запросу. Отличие от обычного хостинга в том, что у лямбды нет состояния: она запускается на короткое время и умирает. В зависимости от окружения лямбда может умереть не сразу, и если ее дернуть повторно, сработает уже запущенный экземпляр. На базе этого делают “прогрев” лямбд, у которых окружение стартует медленно, например Java. В моем случае граальный бинарник быстрый как не весть что, прогрева ему не надо.

    Лямбду можно вызывать разными способами, в том числе HTTP-запросом. У каждой лямбды свой урл, который может быть приватным и публичным. Его можно указать в разных сервисах, что отлично подходит для ботов.

    По сравнению с VM-хостингом у лямбды следующие плюсы:

    • не нужна виртуальная машина и ее настройка;
    • не нужен домен;
    • лямбды экстремально дешевы, потому оплачивается только время работы с округлением до 0.1 секунды;
    • лямбды поощряют писать код без компонентов и состояния, что как бальзам на душу после проектов по работе.

    Поначалу я был крайне скептичен к лямбдам, но теперь вижу в них особую прелесть. Единственный момент — пришлось написать код, чтобы подружить Кложу и Ring с окружением лямбды. Также поправил компиляцию Граалем, чтобы в бинарнике была верная кодировка. В будущем я планирую вынести код Яндекс.Облака в отдельную библиотеку, чтобы избавиться от копипасты.

    Форма поддерживает синтаксис Markdown. Пока что нет кнопок выделения текста болдом и италиком, сделаю потом. Форма отправляется чистым POST, никаких аяксов, хотя первую версию я сделал на fetch, JSON и CORS. Про CORS я все забыл, и пришлось читать свою же статью о том, как все настроить.

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

    Первые комментарии я получил от кого? Правильно, от спамеров. Этим утром предложили виагру, казино и просто левые ссылки. Вот ссылка на уже закрытый RP и его содержимое:

    ---
    id: 1663659055173
    is_spam: false
    is_deleted: false
    post: /bookshelf/
    date: 2022-09-20 07:30:55 +0000
    author_fullname: 'epiehuliqukib'
    ---
    
    http://slkjfdf.net/ - Tatacatay <a href="http://slkjfdf.net/">Aebaluk</a>
    lwk.wlgg.grishaev.me.mrq.mn http://slkjfdf.net/
    

    Очевидно, нужно вводить капчу для защиты от ботов. Я рассматриваю это как новый вызов: как сделать капчу без Javascript в статичном блоге? Уже есть мыслишки, расскажу в четвертой части эпопеи.

    Наконец, последний тезис — зачем вы это делаете, мистер Андерсон? Зачем эти отказы, костыли, превозможения? Ответ — потому что могу. Просто хочется экспериментов. Надоели тормозные сайты с JS, хочу маленький оазис в своем огороде.

  • REPL, Cider, Emacs (часть 4/4)

    Все части

    Оглавление

    Отладка в Cider

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

    Теперь когда вы знакомы с самодельным отладчиком, рассмотрим, что предлагает Cider. В нашем распоряжении два тега: #break и #dbg. Первый тег означает точку останова (брейкпоинт) в месте, где он расположен. Поставьте #break в середину произвольного кода. Перед тем как запустить код, выполните его командой cider-eval-..., иначе эффект не вступит в силу.

    Тег #break ссылается на функцию breakpoint-reader из модуля cider.nrepl.middleware.debug. Она принимает форму и добавляет в ее метаданные особое поле – признак отладки. Далее сработает оснащение (или инструментирование) — алгоритм, который ищет отмеченные формы и оборачивает их кодом, который запускает отладку.

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

    В Emacs нет графических средств отладки. Информация выводится либо рядом с кодом, либо в отдельных буферах. В режиме отладки файл нельзя редактировать; клавиши не вводят текст, а вызывают команды. Ошибка новичков в том, что, попав в отладку, они нажимают все подряд, и процесс протекает с ошибками. Ниже мы рассмотрим процедуру так, чтобы с вами этого не случилось.

    Read more →

  • Деджаваскриптизиция (2)

    Сделал то, что намеревался: удалил с сайта все сторонние сервисы. Больше нет социальных кнопок, гугло-аналитики и, самое важное, комментариев Disqus. На страницах вообще нет аякса за тем исключением, когда встроен плеер Ютуба. Красота.

    Отдельно расскажу про комментарии. Теперь они встроены в блог, являются его частью, а не лежат на серверах Disqus. Для этого я написал импорт из XML. Ясное дело, на Кложе, исходники на Гитхабе.

    Если вкратце, код пробегает по XML, строит мапы и индексы по ID. Для каждого комментария он создает файл вроде _comments/2016-08-08-08-27-14.md с содержимым:

    ---
    id: 2826354744
    is_spam: false
    is_deleted: false
    post: /interview/
    date: 2016-08-08T08:27:14Z
    author_fullname: 'Александр'
    author_nickname: 'VinokurovAlexnader'
    author_is_anon: false
    ---
    
    <p>Вопрос "Есть кортеж из трех элементов. Назначить переменным a, b, c его значения".</p>
    <p>У Вас ответ:<br>a, b, c = [1, 2, 3]</p>
    <p>Но это список, а не кортеж. С кортежом будет вот так:<br>a, b, c = (1, 2, 3)</p>
    

    Всего таких файлов 993. В шаблон заметки я добавил код для вывода комментариев:

    {% assign comments = site.comments | where:'post', include.permalink %}
    {% if comments.size == 0 %}
    <center>Комментариев пока нет</center>
    {% else %}
    <center>Комментарии</center>
    <div class="comments-block">
      {% for comment in comments %}
        <div class="comment-block">
          <p class="comment-lead">
            <small>
              <em>{{ comment.author_fullname or comment.author_nickname }}, {{ comment.date | date_to_string: "ordinal", "RU" }}</em>
            </small>
          </p>
          <div>{{ comment.content | markdownify }}</div>
        </div>
      {% endfor %}
    </div>
    {% endif %}
    

    Получилось неоптимально, потому что на каждый пост происходит линейный поиск. В идеале нужно один раз построить словарь URL => список комментариев и затем брать оттуда. Но я не настолько хорошо знаю Jekyll и Ruby, чтобы это запилить.

    Ради интереса проверьте страницы, где много комментариев, например про сервис IVI или про интервью. Лично мне результат нравится: чистенький HTML, минимум CSS, все лаконично. На текущий момент не учитывается структура, но технически это возможно: каждый комментарий знает ID родителя, и однажды их вывод можно улучшить.

    Теперь главное: как сделать прием комментариев. Мне накидали советов, плюс я гуглил, и все сводится двум решениям: Гитхаб или self-hosted сервис.

    Напомню, как работает Гитхаб: люди пишут комменты к какому-либо issue. Зная номер issue, легко выгрести комментарии GET-запросом, сгенерить HTML и вставить под заметкой. Выглядит красиво:

    , но меня тревожат две вещи.

    1. Комментарии лежат в чужом сервисе, который может отвалиться из-за санкций или Роскомнадзора.
    2. Обязательна авторизация через Гитхаб — не у всех есть учетная запись.

    Self-hosted-решения вроде Remark42 интересны, но вынуждают держать виртуалку, платить за нее и делать бекапы. Придется возиться.

    Я придумал так. Пишем лямбду для Яндекс.Облака (на Кложе с компиляцией Граалем). Лямбда принимает POST-запросы с сайта. На сайте висит статичная форма с полем action=URL нашей лямбды. Отправить форму может любой желающий, даже тот, у кого нет учетки в Гитхабе, Твиттере, Инстаграме и так далее.

    Получив запрос, лямбда создает pull request в репозитории блога. Для этого не нужен git: все возможности Гитхаба доступны в REST API. Pull request содержит новый файл с комментарием в папке _comments. Мне приходит письмо. Если норм, я нажимаю Merge, блог собирается и выкатывается новая версия.

    Одновременно получается модерация: если кто-то написал дичь, я отклоняю PR, и делу конец.

    Открыв PR, лямбда перенаправляет пользователя обратно в блог со словами “спасибо, ваш комментарий скоро появится”.

    Вот такую штуку я хочу запилить. А пока что комментации работают только в Телеграме.

  • Деджаваскриптизиция

    Как вы думаете, сколько запросов делает браузер, пока вы читаете заметку в моем блоге? Я тоже не знал. Десять, двадцать? Что гадать, если можно проверить!

    Открываем консоль разработчика, вкладка Network. Виден всякий ajax/js-хлам. Тык правой кнопкой → “Save all as HAR with content”:

    Получаем жирный JSON со всеми запросами. Вот как выдрать из него урлы:

    cat grishaev.me.har | jq '.log.entries[].request.url' | sort > urls.txt
    

    Если открыть urls.txt и удалить адреса с доменом grishaev.me (коих 5 штук), получится… 69 запросов. Вот они:

    https://a.disquscdn.com/1662551939/images/noavatar92.png
    https://a.disquscdn.com/1662551939/images/noavatar92.png
    https://accounts.google.com/o/oauth2/iframe
    https://accounts.google.com/o/oauth2/iframerpc?action=checkOrigin&origin=https%3A%2F%2Fdisqus.com&client_id=508198334196-bgmagrg0a2rub674g0shidj8fnd50dji.apps.googleusercontent.com
    https://accounts.google.com/o/oauth2/iframerpc?action=issueToken&response_type=token%20id_token&login_hint=AJDLj6J0ayD55kiHpX6FEyK8AjchAx-IxO13rW-0myyrGzenEj7YZFquZJE4XTK6YDPt86Y7SJkVEXs3QpNMeay0bCjiNqDIwg&client_id=508198334196-bgmagrg0a2rub674g0shidj8fnd50dji.apps.googleusercontent.com&origin=https%3A%2F%2Fdisqus.com&scope=profile%20email&ss_domain=https%3A%2F%2Fdisqus.com&include_granted_scopes=true
    https://apis.google.com/_/scs/abc-static/_/js/k=gapi.lb.en.z9QjrzsHcOc.O/m=auth2/rt=j/sv=1/d=1/ed=1/rs=AHpOoo8359JQqZQ0dzCVJ5Ui3CZcERHEWA/cb=gapi.loaded_0?le=scs
    https://apis.google.com/js/api.js
    https://c.disquscdn.com/get?url=https%3A%2F%2Fgrishaev.me%2Fassets%2Fstatic%2Faws%2Fbw%2Fui.png&key=5su7aFyKRIY_tqfWlxdSzw&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fgrishaev.me%2Fassets%2Fstatic%2Faws%2Fmail%2F2.png&key=P-FCJdQOuptvsG613K8vJA&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Fexcel%2Fgood.jpg&key=8OltNCXozdgFxMHmgBTDtQ&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Ftele%2Fscan.jpg&key=3S4NMvR9Sa2dqAik9FSSBw&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Fvlc%2Feat.png&key=_fOEu4w6cw03wCawGGrA_Q&h=200
    https://c.disquscdn.com/next/current/embed/lang/ru.js
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/funny-512x512.png
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/sad-512x512.png
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/upvote-512x512.png
    https://c.disquscdn.com/next/current/recommendations/lang/ru.js
    https://c.disquscdn.com/next/embed/alfie_v4.63f1ab6d6b9d5807dc0c94ef3fe0b851.js
    https://c.disquscdn.com/next/embed/assets/font/icons.4cc7a703d2fdfe684151ff8ac24d45f1.woff2
    https://c.disquscdn.com/next/embed/assets/img/disqus-social-icon-dark.a621bea3e02c9fa04fd3965a3d6f424d.svg
    https://c.disquscdn.com/next/embed/assets/img/loader.ba7c86e8b4b6135bb668d05223f8f127.gif
    https://c.disquscdn.com/next/embed/assets/img/sprite.ad630a07080a45451f139a7487853ff8.png
    https://c.disquscdn.com/next/embed/assets/img/svg-sprite.4da5413f5086c5755b46094b813dbfcd.svg
    https://c.disquscdn.com/next/embed/common.bundle.33bc87b2c4f9324203cc85b7dd1d0492.js
    https://c.disquscdn.com/next/embed/common.bundle.33bc87b2c4f9324203cc85b7dd1d0492.js
    https://c.disquscdn.com/next/embed/lounge.bundle.8d28276e15f31af0eebfd934278922d1.js
    https://c.disquscdn.com/next/embed/lounge.bundle.8d28276e15f31af0eebfd934278922d1.js
    https://c.disquscdn.com/next/embed/lounge.load.0837a7fb2afa86b68e4ee5098ec9905b.js
    https://c.disquscdn.com/next/embed/styles/lounge.4ceaf0673822a0def820ebdc38d84415.css
    https://c.disquscdn.com/next/embed/styles/lounge.4ceaf0673822a0def820ebdc38d84415.css
    https://c.disquscdn.com/next/recommendations/assets/img/img-placeholder.df52e7638153b73862008d3d0556fdda.png
    https://c.disquscdn.com/next/recommendations/common.bundle.a59fbd11efae764ccd959d61e4925fee.js
    https://c.disquscdn.com/next/recommendations/common.bundle.a59fbd11efae764ccd959d61e4925fee.js
    https://c.disquscdn.com/next/recommendations/recommendations.bundle.926bc472e4859a48daa346b4ba2ab4f4.js
    https://c.disquscdn.com/next/recommendations/recommendations.bundle.926bc472e4859a48daa346b4ba2ab4f4.js
    https://c.disquscdn.com/next/recommendations/recommendations.load.9d352c9674ae8172f8669d3aa3a905e9.js
    https://c.disquscdn.com/next/recommendations/styles/recommendations.10022a97346f1c6e3798931bbd8e4bb5.css
    https://c.disquscdn.com/next/recommendations/styles/recommendations.10022a97346f1c6e3798931bbd8e4bb5.css
    https://cdn.viglink.com/images/pixel.gif?ch=1&rn=8.95348561821992
    https://cdn.viglink.com/images/pixel.gif?ch=2&rn=8.95348561821992
    https://connect.facebook.net/en_US/sdk.js
    https://disqus.com/api/3.0/discovery/listRecommendations.json?forum=igrishaev&thread=url%3Ahttps%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&limit=8&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/forums/details?forum=igrishaev&attach=forumFeatures&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/forums/details?forum=igrishaev&attach=forumFeatures&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/threadReactions/loadReactions?thread=9335567925&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/embed/comments/?base=default&f=igrishaev&t_u=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&t_d=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&t_t=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&s_o=default
    https://disqus.com/next/config.js
    https://disqus.com/next/config.js
    https://disqus.com/next/config.js
    https://disqus.com/recommendations/?base=default&f=igrishaev&t_u=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&t_d=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&t_t=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.
    https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i&display=swap&subset=cyrillic,cyrillic-ext,latin-ext
    https://fonts.gstatic.com/s/ptserif/v17/EJRSQgYoZZY2vCFuvAnt66qSVyvVp8NA.woff2
    https://fonts.gstatic.com/s/ptserif/v17/EJRVQgYoZZY2vCFuvAFSzr-_dSb_nco.woff2
    https://fonts.gstatic.com/s/ptserif/v17/EJRVQgYoZZY2vCFuvAFWzr-_dSb_.woff2
    https://glitter.services.disqus.com/urls/?callback=dsqGlitterResponseHandler&forum_shortname=igrishaev&thread_id=9335567925&referer=https%3A%2F%2Fgrishaev.me%2F
    https://igrishaev.disqus.com/embed.js
    https://igrishaev.disqus.com/recommendations.js
    https://io.narrative.io/?companyId=19&id=disqus_id%3Ac6g0c4dk2irns7l&ret=img&ref=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F
    https://links.services.disqus.com/api/ping
    https://live.rezync.com/pixel.html?c=4656c20ee35215f78e9273796625d90b&cid=c6g0c4dk2irns7l&pctry=RU&referrer=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F
    https://mc.yandex.ru/metrika/tag.js
    https://pippio.com/api/sync?pid=1391&ref=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&it=1&iv=c6g0c4dk2irns7l
    https://referrer.disqus.com/juggler/event.gif?abe=1&embed_hidden=0&load_time=1311&event=init_embed&thread=9335567925&forum=igrishaev&forum_id=3964395&imp=2l7278t3kf6iha&prev_imp&thread_slug=zippo_additions_to_the_standard_clojurezip_package&user_type=anon&referrer=https%3A%2F%2Fgrishaev.me%2F&theme=next&dnt=0&tracking_enabled=1&experiment=network_default&variant=fallthrough&service=dynamic&promoted_enabled=true&max_enabled=true
    https://referrer.disqus.com/juggler/event.js?experiment=network_default&variant=fallthrough&page_referrer=https%3A%2F%2Fgrishaev.me%2F&product=embed&thread=9335567925&thread_id=9335567925&forum=igrishaev&forum_id=3964395&zone=thread&page_url=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&service=dynamic&verb=view&object_type=product&object_id=embed&extra_data=%7B%22color_scheme%22%3A%22light%22%2C%22anchor_color%22%3A%22rgb(42%2C122%2C226)%22%2C%22typeface%22%3A%22serif%22%2C%22width%22%3A740%7D&event=activity&imp=2l7278t3kf6iha&prev_imp=&section=default&area=n%2Fa
    https://referrer.disqus.com/juggler/stat.gif?event=lounge.loading.view
    https://www.google-analytics.com/analytics.js
    https://www.gstatic.com/_/mss/boq-identity/_/js/k=boq-identity.IdpIFrameHttp.en_US.z2a12LkW96g.es5.O/d=1/rs=AOaEmlHeSnuHrM44y1tAD9SSj44ODEuRFQ/m=base
    https://yastatic.net/es5-shims/0.0.2/es5-shims.min.js
    https://yastatic.net/share2/share.js
    

    Да, Карл: шестьдесят девять запросов на всякие аналитики, кнопки шаринга и комментарии Disqus. Не знаю, почему некоторые урлы повторяются, выяснять это нет никакого желания. Важное уточнение: 69 – это только в начале. Если промотать страницу и написать комментарий, подгрузится еще пачка скриптов.

    С Disqus вообще беда: с недавних пор они внедрили рекламу. Блокировщики справляются, но как-то раз я зашел на сайт из голого Сафари и о…уел от того, что творит Disqus на странице:

    На мобиле еще хуже: баннеры выстраиваются вертикально, и нужно мотать два экрана.

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

    Ясно одно: это не правильно, нужно исправлять.

    Социальные кнопки и гугло-аналитику банально удалю. За кнопки мне стыдно, потому что это рудимент нулевых. Сегодня они встречаются разве что на сайтах, сделанных в говно-CMS вроде Джумлы, Друпала и Вордпресса. Аналитика была б хороша, если б Гугл не плевал на все этические нормы. Выбирая между желанием посмотреть, кто и что читает и приватностью читателей, я предпочту второе.

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

    У меня возникла идея перенести комментарии Disqus в блог. Технически это возможно: в админке Disqus выгружаем все комментарии, получаем жирный XML-файл (про JSON ребята не слышали). Далее пишем скрипт, который обходит XML и дописывает в файлы с постами. Однажды я уже делал так, когда мигрировал с Эгеи (движка на PHP), только вместо XML был дамп MySQL.

    Остался последний вопрос: как сделать прием комментариев без Disqus? У меня есть одна мысль, но я опишу ее в другой раз, а пока что послушаю ваши советы.

  • REPL, Cider, Emacs (часть 3/4)

    Все части

    Оглавление

    Пространства имен

    При работе с REPL мы всегда находимся в каком-то пространстве. По умолчанию оно написано в приглашении:

    user=> (+ 1 2)
    

    Если перейти в другое пространство, изменится и приглашение:

    user=> (in-ns 'foobar)
    foobar=>
    

    Код, что мы вводим в REPL, вычисляется в текущем пространстве. Если объявить в модуле user переменную:

    (in-ns 'user)
    (def number 1)
    

    , а затем сослаться на нее в пространстве foobar, получим ошибку, что символ number не найден в текущем контексте:

    (in-ns 'foobar)
    (+ 1 number)
    
    Syntax error compiling at (repl-chapter:localhost:53495(clj)*:1:8440).
    Unable to resolve symbol: number in this context
    

    Другой пример: объявим в модулях user и foobar переменные number со значениями 1 и 2. Теперь одна и та же форма (inc number) даст разный результат в зависимости от того, какое пространство текущее. Поэтому перед вычислением мы должны убедиться, что находимся в нужном пространстве имен.

    Чтобы уберечь нас от подобных ошибок, nREPL учитывает параметр ns в сообщениях. Когда мы выполняем код при помощи cider-eval-..., в сообщении, помимо полей op и code, передается ns. Его значение Cider находит из формы (ns...) в начале файла. Вычисляя форму, сервер временно меняет пространство, и результат совпадает с тем, что ожидают.

    Read more →

  • Zippo: additions to the standard clojure.zip package.

    The clojure.zip package is a masterpiece yet misses some utility functions. For example, finding locations, bulk updates, lookups, breadth-first traversing and so on. Zippo, the library I’m introducing in this post, brings some bits of missing functionality.

    Installation

    Lein:

    [com.github.igrishaev/zippo "0.1.0"]
    

    Deps.edn

    {com.github.igrishaev/zippo {:mvn/version "0.1.0"}}
    

    Usage & examples

    First, import both Zippo and clojure.zip:

    (ns zippo.core-test
      (:require
       [clojure.zip :as zip]
       [zippo.core :as zippo]))
    

    Declare a zipper:

    (def z
      (zip/vector-zip [1 [2 3] [[4]]]))
    

    Now check out the following Zippo functions.

    A finite seq of locations

    The loc-seq funtion takes a location and returns a lazy seq of locations untill it reaches the end:

    (let [locs (zippo/loc-seq z)]
      (mapv zip/node locs))
    
    ;; get a vector of notes to reduce the output
    [[1 [2 3] [[4]]]
     1
     [2 3]
     2
     3
     [[4]]
     [4]
     4]
    

    This is quite useful to traverse a zipper without keeping in mind the ending condition (zip/end?).

    Finding locations

    The loc-find function looks for the first location that matches a predicate:

    (let [loc (zippo/loc-find
               z
               (fn [loc]
                 (-> loc zip/node (= 3))))]
    
      (is (= 3 (zip/node loc))))
    

    Above, we found a location which node equals 3.

    The loc-find-all function finds all the locatins that match the predicate:

    (let [locs (zippo/loc-find-all
                z
                (zippo/->loc-pred (every-pred int? even?)))]
    
      (is (= [2 4]
             (mapv zip/node locs))))
    

    Since the predicate accepts a location, you can check its children, siblings and so on. For example, check if a location belongs to a special kind of parent.

    However, most of the time you’re interested in a value (node) rather than a location. The ->loc-pred function converts a node predicate, which accepts a node, into a location predicate. In the example above, the line

    (zippo/->loc-pred (every-pred int? even?))
    

    makes a location predicate which node is an even integer.

    Updating a zipper

    Zippo offers some functions to update a zipper.

    The loc-update one takes a location predicate, an update function and the rest arguments. Here is how you douple all the even numbers in a nested vector:

    (let [loc
          (zippo/loc-update
           z
           (zippo/->loc-pred (every-pred int? even?))
           zip/edit * 2)]
    
      (is (= [1 [4 3] [[8]]]
             (zip/root loc))))
    

    For the updating function, one may use zip/append-child to append a child, zip/remove to drop the entire location and so on:

    (let [loc
          (zippo/loc-update
           z
           (fn [loc]
             (-> loc zip/node (= [2 3])))
           zip/append-child
           :A)]
    
      (is (= [1 [2 3 :A] [[4]]]
             (zip/root loc))))
    

    The node-update function is similar but acts on nodes. Instead of loc-pred and loc-fn, it accepts node-pred and node-fn what operate on nodes.

    (let [loc
        (zippo/node-update
         z
         int?
         inc)]
    (is (= [2 [3 4] [[5]]]
           (zip/root loc))))
    

    Slicing a zipper by layers

    Sometimes, you need to slice a zipper on layers. This is what is better seen on a chart:

         +---ROOT---+    ;; layer 1
         |          |
       +-A-+      +-B-+  ;; layer 2
       | | |      | | |
       X Y Z      J H K  ;; layer 3
    
    • Layer 1 is [Root];
    • Layer 1 is [A B];
    • Layer 3 is [X Y Z J H K]

    The loc-layers function takes a location and builds a lazy seq of layers. The first layer is the given location, then its children, the children of children and so on.

    (let [layers
          (zippo/loc-layers z)]
    
      (is (= '(([1 [2 3] [[4]]])
               (1 [2 3] [[4]])
               (2 3 [4])
               (4))
             (for [layer layers]
               (for [loc layer]
                 (zip/node loc))))))
    

    Breadth-first seq of locations

    The clojure.zip package uses depth-first method of traversing a tree. Let’s number the items:

           +-----ROOT[1]----+
           |                |
     +----A[2]---+     +---B[6]--+
     |     |     |     |    |    |
     X[3] Y[4] Z[5]   J[7] H[8] K[9]
    

    This sometimes may end up with an infinity loop when you generate children on the fly.

    The loc-seq-breadth functions offers the opposite way of traversing a zipper:

           +-----ROOT[1]----+
           |                |
     +----A[2]---+     +---B[3]--+
     |     |     |     |    |    |
     X[4] Y[5] Z[6]   J[7] H[8] K[9]
    

    This is useful to solve some special tasks related to zippers.

    Lookups

    When working with zippers, you often need such functionality as “go up/left/right until meet something”. For example, from a given location, go up until a parent has a special attribute. Zippo offers four functions for that, namely lookup-up, lookup-left, lookup-right, and lookup-down. All of them take a location and a predicate:

    (let [loc
          (zip/vector-zip [:a [:b [:c [:d]]] :e])
    
          loc-d
          (zippo/loc-find loc
                          (zippo/->loc-pred
                           (fn [node]
                             (= node :d))))
    
          loc-b
          (zippo/lookup-up loc-d
                           (zippo/->loc-pred
                            (fn [node]
                              (and (vector? node)
                                   (= :b (first node))))))]
    
      (is (= :d (zip/node loc-d)))
    
      (is (= [:b [:c [:d]]] (zip/node loc-b))))
    

    In the example above, first we find the :d location. From there, we go up until we meet [:b [:c [:d]]]. If there is no such a location, the result will be nil.

    Also See

    The code from this library was used for Clojure Zippers manual – the complete guide to zippers in Clojure from the very scratch.

    © 2022 Ivan Grishaev

Страница 29 из 87