Около года назад Github запустил функцию, которая позволяет добавлять README в профиль пользователя. Чтобы добавить README в свой профиль, вам необходимо:


  • создайте общедоступный репозиторий с именем, совпадающим с вашим именем пользователя Github.

  • поместите README.md в корень репозитория

Вы можете узнать больше об этом в документации Github.


Что такое динамический профиль Github?


Динамический профиль Github обновляется автоматически по какому-либо внешнему событию или по расписанию. Это возможно с использованием Github Actions. Github Actions — еще одна недавно выпущенная функция Github. Github Actions — это, по сути, система CI/CD, которая позволяет создавать и запускать настраиваемые рабочие процессы.


Впервые я узнал о профиле README в этой статье на Hackernoon. Парень использовал PHP для получения и обновления списка последних сообщений в своем блоге. Хотя я эксперт по PHP, я хотел сделать его более сложным. Я понял, что синтаксический анализ XML и замена текста в файле достижимы только с помощью собственных инструментов Bash.


Парсинг RSS-канала


RSS-канал — это обычный XML-файл с простой схемой. Вот пример из моего только что запущенного блога:


```разметка




<канал>


Сообщения в техническом блоге Александра Табакова | атабаков


<ссылка>https://atabakoff.com/posts/


Недавний контент в сообщениях технического блога Александра Табакова | атабаков


<изображение>


https://atabakoff.com/aleksandr_tabakov.jpeg


<ссылка>https://atabakoff.com/aleksandr_tabakov.jpeg



Хьюго -- gohugo.io


Пт, 27 мая 2022 г., 21:33:51 +0200


<элемент>


Как запустить Vaultwarden в Docker/Podman как службу systemd


https://atabakoff.com/how-to-run-vaultwarden-in-podman-as-a-systemd-service/


Пт, 27 мая 2022 г., 21:33:51 +0200


https://atabakoff.com/how-to-run-vaultwarden-in-podman-as-a-systemd-service/


Запуск Vaultwarden в контейнере в качестве службы systemd с использованием Podman. Как установить Podman, запустить Vaultwarden в контейнере, создать конфигурацию systemd для службы Vaultwarden и управлять ею с помощью systemctl.





Каждое сообщение представлено элементом item, где нам нужны title, link и pubDate.


Парсинг RSS-канала с помощью grep


Наивный подход состоит в том, чтобы использовать grep, а затем построить уценку в цикле bash. Давайте сначала попробуем grep:


``` ударить


wget --quiet -O rss.xml https://atabakoff.com/posts/index.xml


кот rss.xml | grep -Po '<(название|ссылка|дата публикации)>[^<]+'


Сообщения в техническом блоге Александра Табакова | атабаков</p><br><p><ссылка>https://atabakoff.com/posts/</p><br><p><ссылка>https://atabakoff.com/aleksandr_tabakov.jpeg</p><br><p><title>Как запустить Vaultwarden в Docker/Podman в качестве службы systemd</p><br><p><ссылка>https://atabakoff.com/how-to-run-vaultwarden-in-podman-as-a-systemd-service/</p><br><p><pubDate>Пт, 27 мая 2022 г. 21:33:51 +0200</p><br><p>Неплохо, но нам нужно избавиться от первых трех строк и открывающего тега:</p><br><p>``` ударить</p><br><p>кот rss.xml | grep -Po '<(title|link|pubDate)>[^<]+' | хвост -n +4 \</p><br><p>| grep -oE '>([^>]+)' | grep -oE '([^>]+)'</p><br><p>Как запустить Vaultwarden в Docker/Podman в качестве службы systemd</p><br><p>https://atabakoff.com/how-to-run-vaultwarden-in-podman-as-a-systemd-service/</p><br><p>Пт, 27 мая 2022 21:33:51 +0200</p><br><p>Просто для проверки grep</p><br><p>https://atabakoff.com/testing-grep/</p><br><p>Пт, 31 мая 2022 18:33:51 +0200</p><br><p>Я добавил один дополнительный элемент, чтобы проверить, работает ли мое выражение с несколькими сообщениями. В этот момент я начинаю думать, что <code>grep</code> может быть не лучшим вариантом. Я быстро написал конвертер для преобразования RSS в уценку, прежде чем исследовать другие варианты:</p><br><p>``` ударить</p><br><h1>!/бин/баш</h1><br><p>items=$( cat rss.xml | grep -Po '<(title|link|pubDate)>[^<]+' | tail -n +4 \</p><br><p>| grep -oE '>([^>]+)' | grep -oE '([^>]+)' )</p><br><p>ЕСЛИ=$'<br>'</p><br><p>количество = 0</p><br><p>для товара в $items</p><br><p>делать</p><br><p>case $(expr $count % 3) в</p><br><p>'0')</p><br><p>название = $ элемент</p><br><p>ссылка = ''</p><br><p>дата публикации = ''</p><br><p>'1')</p><br><p>ссылка=$элемент</p><br><p>'2')</p><br><p>pubDate=$(дата -d "$item" +'%d/%m/%Y' )</p><br><p>кошка<<EOF</p><br><ul> <li>$pubDate <a href="$ссылка">$название</a></li> </ul><br><p>EOF</p><br><p>эсак</p><br><p>количество = $ (($ количество + 1))</p><br><p>Выполнено</p><br><p>Запустите его для проверки:</p><br><p>``` ударить</p><br><p>./test.sh</p><br><ul> <li>05.07.2022 <a href="https://atabakoff.com/how-to-run-vaultwarden-in-podman-as-a-systemd-service/">Как запустить Vaultwarden в Docker/Podman в качестве службы systemd</a></li> </ul><br><ul> <li>05.03.2022 <a href="https://atabakoff.com/testing-grep/">Только для проверки grep</a></li> </ul><br><h3>Проверка производительности</h3><br><p>Моя RSS-лента крошечная. Чтобы измерить производительность, нам нужно запустить парсер много раз. Я создал файл <code>test.sh</code>:</p><br><p>``` ударить</p><br><h1>!/бин/баш</h1><br><p>х=100</p><br><p>в то время как [ "$x" -gt "0" ] ; делать</p><br><p>$(/bin/bash $1) 2>/dev/null</p><br><p>х=$((х-1))</p><br><p>Выполнено</p><br><p>Он принимает файл сценария в качестве параметра и запускает его 100 раз в цикле. Давайте запустим его с <code>time</code>, чтобы увидеть, сколько времени требуется для анализа фида:</p><br><blockquote> <p>время ./test.sh grep-rss.sh</p> </blockquote><br><p>./test.sh grep.sh 1,87 с пользователь 0,72 с система 137% ЦП 1,883 всего</p><br><p>Не очень впечатляюще, но ожидаемо из-за использования регулярных выражений.</p><br><h3>Парсинг RSS-канала в Bash</h3><br><p>Я начал гуглить, есть ли способ разобрать XML в Bash, и нашел это [отличное решение] (https://www.linuxjournal.com/content/parsing-rss-news-feed-bash-script). Здесь описана та же проблема парсинга RSS-канала. Я изменил код для своих нужд и сохранил его в файле <code>parse-rss.sh</code>:</p><br><p>``` ударить</p><br><h1>!/бин/баш</h1><br><p>xmlgetnext () {</p><br><p>местный IFS='>'</p><br><p>read -d '<' ЗНАЧЕНИЕ ТЕГА</p><br><p>кот $1 | пока xmlgetnext ; делать</p><br><p>случай $TAG в</p><br><p>'вещь')</p><br><p>название = ''</p><br><p>ссылка = ''</p><br><p>дата публикации = ''</p><br><p>'заглавие')</p><br><p>название=$ЗНАЧЕНИЕ</p><br><p>'ссылка на сайт')</p><br><p>ссылка=$ЗНАЧЕНИЕ</p><br><p>'дата публикации')</p><br><p>pubDate=$(дата -d "$VALUE" +'%d/%m/%Y' )</p><br><p>'/вещь')</p><br><p>кошка<<EOF</p><br><ul> <li>$pubDate <a href="$ссылка">$название</a></li> </ul><br><p>EOF</p><br><p>эсак</p><br><p>Выполнено</p><br><p>Я провел тот же тест, чтобы сравнить производительность:</p><br><p>``` ударить</p><br><p>время ./test.sh parse-rss.sh</p><br><p>./test.sh parse.sh 0,81 с пользователь 0,33 с система 109% ЦП 1,042 всего</p><br><p>Почти <em>в два раза</em> быстрее: «1042» против «1883». Это последний подход, который я выбрал для обработки RSS-канала.</p><br><h2>Обновление README.md</h2><br><p>Обновление списка постов — это просто замена. Поскольку уценка позволяет использовать HTML-код, мы можем использовать HTML-комментарии, чтобы пометить заполнитель для сообщений:</p><br><p>```разметка</p><br><!--блог:старт--><br><!--блог:конец--><br><p>Стандартным инструментом для замены текста в Bash является <code>sed</code>, но у него есть одно ограничение. Это строковый редактор, обрабатывающий только одну строку за один шаг. В нашем случае и заполнитель, и список постов представляют собой многострочный текст. Вот как я это решил:</p><br><p>``` ударить</p><br><h1>!/бин/баш</h1><br><p>ЧИСЛО=$(($2*3))</p><br><p>POSTS=$(cat $1 | head $NUM | tr '<br>' '\t')</p><br><p>кот README.md | тр '<br>' '\t' \</p><br><p>| sed -E "s#(<!--блог:начало-->).*(<!--блог:конец-->)#\1\t${Публикации}\2#g" \</p><br><p>| tr '\t' '<br>' > README.tmp</p><br><p>mv README.tmp README.md</p><br><p>rm -f rss.xml posts.md</p><br><p>Некоторые вещи, которые стоит пояснить:</p><br><ul> <li><code>NUM=$(($2*3))</code> — количество строк для указанного количества постов; в моем случае я хочу показать пять постов по три строки каждый (название, ссылка, дата)</li> </ul><br><ul> <li><code>tr '<br>' '\t'</code> должен преобразовать текст в одну строку для обработки с помощью <code>sed</code></li> </ul><br><ul> <li><code>tr '\t' '<br>'</code> возвращает символы новой строки</li> </ul><br><h2>Конвейер действий Github</h2><br><p>Теперь у нас есть наши сценарии, и нам нужно поместить их в конвейер. Действия Github просматривают специальный каталог <code>.gihub/workflows</code> и обрабатывают там каждый файл <code>.yaml</code>. Я создал там файл <code>posts.yml</code> со следующим содержимым:</p><br><p>``ямл</p><br><p>name: Обновление сообщений в блоге</p><br><p>на:</p><br><p>толкать:</p><br><p>workflow_dispatch:</p><br><p>расписание:</p><br><ul> <li>cron: '0 0 * <em> </em>'</li> </ul><br><p>вакансии:</p><br><p>обновить-readme-с-последними-сообщениями:</p><br><p>запуски: ubuntu-последняя</p><br><p>шаги:</p><br><ul> <li>имя: Клонировать репозиторий</li> </ul><br><p>использует: action/checkout@v2</p><br><p>с:</p><br><p>глубина выборки: 1</p><br><ul> <li>имя: Получить RSS-канал</li> </ul><br><p>выполнить: wget --quiet -O rss.xml https://atabakoff.com/posts/index.xml</p><br><ul> <li>название: Разобрать RSS-канал</li> </ul><br><p>запустить: |</p><br><p>компакт-диск ${GITHUB_WORKSPACE}</p><br><p>./src/parse-rss.sh rss.xml > posts.md</p><br><ul> <li>название: Обновление README.md</li> </ul><br><p>запустить: |</p><br><p>компакт-диск ${GITHUB_WORKSPACE}</p><br><p>./src/update-readme.sh posts.md 5</p><br><ul> <li>имя: Нажмите изменения</li> </ul><br><p>запустить: |</p><br><p>git config --global user.name "${GITHUB_ACTOR}"</p><br><p>git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"</p><br><p>git commit -am "Обновленные сообщения в блоге" | выход 0</p><br><p>git push</p><br><p>Вот что нужно пояснить:</p><br><ul> <li><code>push</code> - запускать по нажатию</li> </ul><br><ul> <li><code>cron: '0 0 * * *'</code> - это расписание, в моем случае каждый день в полночь</li> </ul><br><ul> <li><code>uses: action/checkout@v2</code> клонирует репозиторий</li> </ul><br><p>Затем я разделил получение, синтаксический анализ и обновление на отдельные этапы. Это позволяет мне быстро локализовать проблему, если что-то пойдет не так. Стоит отметить:</p><br><ul> <li><code>cd ${GITHUB_WORKSPACE}</code> должен перейти в текущий рабочий каталог, который является недавно клонированным репозиторием.</li> </ul><br><ul> <li><code>${GITHUB_ACTOR}</code> – ваше имя пользователя</li> </ul><br><ul> <li><code>${GITHUB_ACTOR}@users.noreply.github.com</code> — это специальный адрес электронной почты Github, который можно использовать для отправки изменений в репозиторий.</li> </ul><br><h2>Вывод</h2><br><p>Вы можете найти полное решение в <a href="https://github.com/daydiff/daydiff/">репозиторий моего профиля</a>. Было очень весело решать эту проблему с помощью чистого Bash.</p><br><p>При этом на Github существует множество действий сообщества. Они позволяют создать динамический профиль без написания кода. Все, что вам нужно сделать, это написать немного YAML. Но в этом мало сложностей. Это не путь воина.</p><br><p>Также опубликовано [здесь] (https://atabakoff.com/creating-a-dynamic-github-profile-with-github-actions-and-bash/).</p><br> </div> <!-- Аудио-версия --> <!-- Теги --> <!-- Prev / Next --> <div class="pn"> <a class="pn-card" href="/blog/smart-contract-security-is-complicated-but-totally-worth-it/"> <div class="lbl">← предыдущая</div> <div class="t">Безопасность смарт-контрактов сложна, но того стоит</div> </a> <a class="pn-card right" href="/blog/mcp-is-dead-ili-pochemu-protokol-konteksta-modelei-tolko-nachinaet-svoi-put/"> <div class="lbl r">следующая →</div> <div class="t">MCP is dead? Или почему протокол контекста моделей только начинает свой путь</div> </a> </div> <!-- Комментарии --> <section class="panel comments-section"> <div class="comments-hd"> <span class="title">комментарии · 0</span> </div> <div class="login-prompt"> // чтобы оставить комментарий — <a href="/login/?next=/blog/using-github-actions-and-bash-to-create-dynamic-github-profile/">войдите в систему</a> или <a href="/register/">зарегистрируйтесь</a> </div> </section> </div><!-- /.article --> <!-- ── Sidebar ── --> <aside> <!-- TOC (генерируется JS) --> <section class="panel toc" id="toc-panel"> <div class="panel-hd"> <span class="title">оглавление</span> <span class="meta" id="toc-count">// 0 разделов</span> </div> <div class="panel-bd-flush"> <ol class="toc" id="toc-list"> <li style="color:var(--fg-mute);font-size:11px;letter-spacing:.04em;"> // генерируется… </li> </ol> </div> </section> <!-- Свежее --> <section class="panel"> <div class="panel-hd"> <span class="title">свежее</span> <span class="meta">// последние публикации</span> </div> <div class="panel-bd-flush"> <div class="recent-list"> <a href="/blog/mcp-is-dead-ili-pochemu-protokol-konteksta-modelei-tolko-nachinaet-svoi-put/"> <div class="rtitle">MCP is dead? Или почему протокол контекста моделей только начинает свой путь</div> <div class="rdate">30.05.2026</div> </a> <a href="/blog/snowboard-kids-2-polnaia-dekompiliatsiia-legendy-n64-i-novye-vozmozhnosti/"> <div class="rtitle">Snowboard Kids 2: Полная декомпиляция легенды N64 и новые возможности</div> <div class="rdate">30.05.2026</div> </a> <a href="/blog/wordle-otvet-segodnia-2026-05-30/"> <div class="rtitle">Ответ Wordle сегодня 30 мая 2026 — слово дня</div> <div class="rdate">30.05.2026</div> </a> <a href="/blog/an-llm-api-call-in-4-gifs-poshagovoe-rukovodstvo/"> <div class="rtitle">An LLM API call, in 4 GIFs: Пошаговое руководство</div> <div class="rdate">30.05.2026</div> </a> <a href="/blog/on-rendering-diffs-polnoe-rukovodstvo/"> <div class="rtitle">On Rendering Diffs: Полное Руководство</div> <div class="rdate">30.05.2026</div> </a> </div> </div> </section> <!-- Популярное всё время --> <section class="panel"> <div class="panel-hd"> <span class="title">топ всех времён</span> <span class="meta">// all time</span> </div> <div class="panel-bd-flush"> <div class="recent-list"> <a href="/blog/marvel-s-wolverine-everything-know-about-exclusive/"> <div class="rtitle">Marvel’s Wolverine: все, что мы знаем об эксклюзиве для PS5 на данный момент</div> <div class="rdate">31.03.2023</div> </a> <a href="/blog/movies-2023-biggest-upcoming-releases-heading-theaters-soon/"> <div class="rtitle">Новые фильмы 2023 года: самые крупные предстоящие релизы скоро появятся в кинотеатрах</div> <div class="rdate">07.12.2022</div> </a> <a href="/blog/netflix-movies-2023-biggest-original-films-coming-streamer/"> <div class="rtitle">Новые фильмы Netflix 2023 года: самые большие оригинальные фильмы, выходящие на стример</div> <div class="rdate">28.12.2022</div> </a> <a href="/blog/8-open-source-projects-to-help-your-business-run-efficiently/"> <div class="rtitle">8 проектов с открытым исходным кодом, которые помогут вашему бизнесу работать эффективно</div> <div class="rdate">06.04.2022</div> </a> <a href="/blog/xbox-series-update-just-dropped-could-save-money/"> <div class="rtitle">Новое обновление Xbox Series X только что вышло и может сэкономить вам деньги</div> <div class="rdate">12.01.2023</div> </a> </div> </div> </section> <!-- Категории --> <section class="panel"> <div class="panel-hd"> <span class="title">категории</span> <span class="meta">// 24</span> </div> <div class="panel-bd-flush"> <div class="cat-list"> <a href="http://coffee-web.ru/blog/category/tehnologii-i-it/"> Технологии и IT <span class="cnt">(27892)</span> </a> <a href="http://coffee-web.ru/blog/category/games-entertainment-hobbies/"> Игры, развлечения и хобби <span class="cnt">(4800)</span> </a> <a href="http://coffee-web.ru/blog/category/artificial-intelligence-and-the-future/"> Искусственный интеллект и будущее <span class="cnt">(294)</span> </a> <a href="http://coffee-web.ru/blog/category/biznes-i-predprinimatelstvo/"> Бизнес и предпринимательство <span class="cnt">(273)</span> </a> <a href="http://coffee-web.ru/blog/category/obshchestvo-i-kultura/"> Общество и культура <span class="cnt">(244)</span> </a> <a href="http://coffee-web.ru/blog/category/disign-and-creative/"> Дизайн и креатив <span class="cnt">(202)</span> </a> <a href="http://coffee-web.ru/blog/category/social-media-and-Internet-culture/"> Социальные медиа и интернет-культура <span class="cnt">(107)</span> </a> <a href="http://coffee-web.ru/blog/category/economics-and-finance/"> Экономика и финансы <span class="cnt">(107)</span> </a> <a href="http://coffee-web.ru/blog/category/razrabotka/"> Разработка <span class="cnt">(85)</span> </a> <a href="http://coffee-web.ru/blog/category/science-and-research/"> Наука и исследования <span class="cnt">(74)</span> </a> <a href="http://coffee-web.ru/blog/category/iskusstvennyi-intellekt/"> Искусственный интеллект <span class="cnt">(62)</span> </a> <a href="http://coffee-web.ru/blog/category/sports-health/"> Спорт и здоровье <span class="cnt">(55)</span> </a> <a href="http://coffee-web.ru/blog/category/novosti/"> Новости <span class="cnt">(53)</span> </a> <a href="http://coffee-web.ru/blog/category/psikhologiia-i-samorazvitie/"> Психология и саморазвитие <span class="cnt">(50)</span> </a> <a href="http://coffee-web.ru/blog/category/bezopasnost/"> Безопасность <span class="cnt">(47)</span> </a> <a href="http://coffee-web.ru/blog/category/zhelezo/"> Железо <span class="cnt">(28)</span> </a> <a href="http://coffee-web.ru/blog/category//"> hackernoon <span class="cnt">(24)</span> </a> <a href="http://coffee-web.ru/blog/category/education-and-training/"> Образование и обучение <span class="cnt">(20)</span> </a> <a href="http://coffee-web.ru/blog/category/startapy/"> Стартапы <span class="cnt">(20)</span> </a> <a href="http://coffee-web.ru/blog/category/marketing-i-reklama/"> Маркетинг и реклама <span class="cnt">(17)</span> </a> <a href="http://coffee-web.ru/blog/category/karera/"> Карьера <span class="cnt">(9)</span> </a> <a href="http://coffee-web.ru/blog/category/travel-and-lifestyle/"> Путешествия и lifestyle <span class="cnt">(6)</span> </a> <a href="http://coffee-web.ru/blog/category/instrumenty/"> Инструменты <span class="cnt">(6)</span> </a> <a href="http://coffee-web.ru/blog/category/devops/"> DevOps <span class="cnt">(2)</span> </a> </div> </div> </section> <!-- LiveInternet counter --> <section class="panel" id="liveinternet-panel"> <div class="panel-hd"> <span class="title">site stats</span> <span class="meta">// liveinternet</span> </div> <div class="panel-bd-flush" style="display:flex;justify-content:center;padding:16px 0;"> <div class="li-counter-wrap"> <!--LiveInternet counter--> <a href="https://www.liveinternet.ru/click" target="_blank" rel="noopener"> <img id="licntCB0E" width="88" height="120" style="border:0;display:block;" title="LiveInternet: показано количество просмотров и посетителей" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7" alt="LiveInternet"/> </a> <!--/LiveInternet--> </div> </div> </section> <script> (function(d,s){ d.getElementById("licntCB0E").src= "https://counter.yadro.ru/hit?t29.14;r"+escape(d.referrer)+ ((typeof(s)=="undefined")?"":";s"+s.width+"*"+s.height+"*"+ (s.colorDepth?s.colorDepth:s.pixelDepth))+ ";u"+escape(d.URL)+";h"+escape(d.title.substring(0,150))+";"+Math.random(); })(document,screen); </script> </aside> </div><!-- /.article-grid --> <footer class="foot"> <div>© coffee-web · 2026 · all signals are noise until proven otherwise</div> <div> <a href="https://t.me/coffeewebru" target="_blank" rel="noopener">telegram</a> </div> </footer> </div> </div> </div> <div id="pjax-scripts"> <script> /* Comment AJAX submit */ (function () { var form = document.getElementById('comment-form'); if (!form) return; form.addEventListener('submit', function (e) { e.preventDefault(); var text = document.getElementById('comment-text').value.trim(); var errEl = document.getElementById('comment-error'); if (!text) { errEl.textContent = '// текст не может быть пустым'; errEl.style.display = 'inline'; return; } errEl.style.display = 'none'; var ct = form.querySelector('[name=content_type]').value; var objectId = form.querySelector('[name=object_id]').value; var csrfToken = form.querySelector('[name=csrfmiddlewaretoken]').value; var btn = form.querySelector('.send-btn'); btn.disabled = true; btn.textContent = 'SENDING...'; fetch('/comments/comments/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken, }, body: JSON.stringify({ text: text, content_type: parseInt(ct), object_id: parseInt(objectId) }), }) .then(function (r) { return r.ok ? r.json() : r.json().then(function(d){ throw d; }); }) .then(function (data) { document.getElementById('comment-text').value = ''; btn.disabled = false; btn.textContent = 'SEND ⏎'; var list = document.querySelector('.comment-list'); if (!list) { list = document.createElement('div'); list.className = 'comment-list'; form.closest('.comments-section').appendChild(list); } var initial = document.querySelector('[name=csrfmiddlewaretoken]'); var now = new Date(); var dateStr = now.toLocaleDateString('ru-RU') + ' ' + now.toLocaleTimeString('ru-RU', {hour:'2-digit',minute:'2-digit'}); var authorInitial = (data.author || '').charAt(0).toUpperCase(); var authorName = data.author || ''; var div = document.createElement('div'); div.className = 'cmt'; div.innerHTML = '<div class="cmt-avatar">' + authorInitial + '</div>' + '<div class="cmt-content">' + '<div class="cmt-head">' + '<span class="user">[' + authorName + '@coffee-web]</span>' + '<span class="ts">' + dateStr + '</span>' + '</div>' + '<div class="cmt-text">' + data.text + '</div>' + '</div>'; list.prepend(div); var countEl = document.querySelector('.comments-hd .title'); if (countEl) { var match = countEl.textContent.match(/\d+/); var cnt = match ? parseInt(match[0]) + 1 : 1; countEl.textContent = 'комментарии · ' + cnt; } }) .catch(function (err) { btn.disabled = false; btn.textContent = 'SEND ⏎'; var msg = err && err.detail ? err.detail : 'ошибка отправки'; errEl.textContent = '// ' + msg; errEl.style.display = 'inline'; }); }); }()); /* Reading progress bar */ (function () { var bar = document.getElementById('reading-bar'); if (!bar) return; window.addEventListener('scroll', function () { var h = document.documentElement; var pct = h.scrollHeight > h.clientHeight ? (h.scrollTop / (h.scrollHeight - h.clientHeight)) * 100 : 0; bar.style.width = pct + '%'; }, { passive: true }); }()); /* Auto-generate TOC from article headings */ (function () { var body = document.getElementById('article-body'); var list = document.getElementById('toc-list'); var count = document.getElementById('toc-count'); if (!body || !list) return; var headings = body.querySelectorAll('h2, h3'); if (!headings.length) { list.innerHTML = '<li style="color:var(--fg-mute);font-size:11px;">// нет разделов</li>'; return; } list.innerHTML = ''; headings.forEach(function (h, i) { var id = 'toc-' + i; h.id = id; var li = document.createElement('li'); li.className = h.tagName === 'H3' ? 'h3' : ''; var a = document.createElement('a'); a.href = '#' + id; a.textContent = h.textContent; li.appendChild(a); list.appendChild(li); }); if (count) count.textContent = '// ' + headings.length + ' разделов'; /* scroll spy */ window.addEventListener('scroll', function () { var active = null; headings.forEach(function (h) { if (h.getBoundingClientRect().top < 120) active = h.id; }); list.querySelectorAll('li').forEach(function (li) { var a = li.querySelector('a'); li.classList.toggle('active', !!(a && active && a.getAttribute('href') === '#' + active)); }); }, { passive: true }); }()); </script> </div> <div id="g-widget" data-chat-url="/api/chat/message/" data-max-msgs="3"> <div id="g-terminal"> <div class="gw-header"> <span>╔═ GL1TCH v0.1 ═[ПОДКЛЮЧЕНО]═╗</span> <span class="gw-close" id="g-close">[×]</span> </div> <div class="gw-status"> <div class="gw-dot"></div> <span>СОЕДИНЕНИЕ АКТИВНО</span> <div class="gw-limit"> <span>запросов:</span> <span class="gw-blocks" id="g-blocks"></span> </div> </div> <div id="g-messages"> <div class="gw-m gw-sys">// сессия #{<span id="g-sid"></span>} начата</div> </div> <div class="gw-input-row"> <span class="gw-prompt">>_</span> <input id="g-input" type="text" placeholder="напиши что-нибудь..." maxlength="400" autocomplete="off"> <button class="gw-send" id="g-send">[ОТПРАВИТЬ]</button> </div> <div id="g-sleep"> <div class="gw-sl-title">[ РАЗРЫВ СВЯЗИ ]</div> <div class="gw-sl-body"> лимит исчерпан...<br> иду спать... zzZ </div> <div class="gw-sl-hint"> хочешь больше? <a href="/register/" class="gw-sl-link">[зарегистрироваться]</a> <span class="gw-sl-note">// +10 запросов в день</span> </div> </div> </div> <div id="g-bubble"><span id="g-btext"></span></div> <div id="g-char" title="GL1TCH"> <svg viewBox="0 0 82 90" xmlns="http://www.w3.org/2000/svg"> <path d="M14 50 Q9 68 7 90 L75 90 Q73 68 68 50 Q58 42 41 42 Q24 42 14 50Z" fill="#0c0c0c" stroke="#00aa29" stroke-width="0.9"/> <path d="M28 68 Q28 76 41 76 Q54 76 54 68 Q54 62 41 62 Q28 62 28 68Z" fill="#090909" stroke="#152515" stroke-width="0.7"/> <path d="M17 46 Q20 20 41 18 Q62 20 65 46 Q58 39 41 37 Q24 39 17 46Z" fill="#111" stroke="#00aa29" stroke-width="0.9"/> <ellipse cx="41" cy="30" rx="19" ry="19" fill="#080808" stroke="#1a3a1a" stroke-width="0.7"/> <path d="M23 28 Q26 41 41 43 Q56 41 59 28 Q56 37 41 38 Q26 37 23 28Z" fill="#040404" opacity=".95"/> <ellipse class="gw-eye" cx="33" cy="29" rx="4.2" ry="3.2" fill="#00ff41"/> <ellipse class="gw-eye gw-eye-r" cx="49" cy="29" rx="4.2" ry="3.2" fill="#00ff41"/> <ellipse cx="33" cy="29" rx="2" ry="2" fill="#002200"/> <ellipse cx="49" cy="29" rx="2" ry="2" fill="#002200"/> <ellipse cx="34.2" cy="27.8" rx="0.9" ry="0.9" fill="#00ff41" opacity=".75"/> <ellipse cx="50.2" cy="27.8" rx="0.9" ry="0.9" fill="#00ff41" opacity=".75"/> <line x1="36" y1="45" x2="34" y2="60" stroke="#152515" stroke-width="1.1"/> <line x1="46" y1="45" x2="48" y2="60" stroke="#152515" stroke-width="1.1"/> <g class="gw-hl"> <rect x="8" y="79" width="26" height="9" rx="1.5" fill="#090909" stroke="#152515" stroke-width="0.8"/> <rect x="10" y="81" width="4" height="2.5" rx=".5" fill="#152515"/> <rect x="16" y="81" width="4" height="2.5" rx=".5" fill="#00ff41" opacity=".5"/> <rect x="22" y="81" width="4" height="2.5" rx=".5" fill="#152515"/> </g> <g class="gw-hr"> <rect x="48" y="79" width="26" height="9" rx="1.5" fill="#090909" stroke="#152515" stroke-width="0.8"/> <rect x="50" y="81" width="4" height="2.5" rx=".5" fill="#00ff41" opacity=".5"/> <rect x="56" y="81" width="4" height="2.5" rx=".5" fill="#152515"/> <rect x="62" y="81" width="4" height="2.5" rx=".5" fill="#152515"/> </g> <line x1="23" y1="29" x2="59" y2="29" stroke="#00ff41" stroke-width="0.3" opacity=".25"/> </svg> </div> </div> <script src="/static/js/glitch_widget.js"></script> <div id="mp-widget"></div> <script src="/static/js/music_player.js"></script> <script src="/static/js/matrix/pjax.js"></script> <script src="/static/js/matrix/rates.js"></script> <script src="/static/js/matrix/tweaks.js"></script> <script> (function () { /* boot art */ var artEl = document.getElementById('boot-art'); if (artEl) artEl.textContent = [ ' ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ', ' █ C O F F E E - W E B :: v4.0 █ ', ' █ secure node // uplink: established █', ' ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ' ].join('\n'); setTimeout(function () { var boot = document.getElementById('boot'); if (!boot) return; boot.classList.add('gone'); setTimeout(function () { if (boot.parentNode) boot.parentNode.removeChild(boot); }, 600); }, 700); /* clock */ function tick() { var el = document.getElementById('hud-clock'); if (el) el.textContent = new Date().toLocaleTimeString('ru-RU', { hour12: false }); } tick(); setInterval(tick, 1000); /* fake HUD metrics */ var cpu = 27, mem = 41, net = 940, online = 2847; function hud() { cpu = Math.max(8, Math.min(94, cpu + (Math.random() - .5) * 14)); mem = Math.max(20, Math.min(78, mem + (Math.random() - .5) * 4)); net = Math.max(120, Math.min(990, net + (Math.random() - .5) * 60)); online = Math.max(100, online + Math.floor((Math.random() - .45) * 14)); var c = document.getElementById('hud-cpu'), m = document.getElementById('hud-mem'), n = document.getElementById('hud-net'), u = document.getElementById('hud-users'); if (c) c.textContent = Math.round(cpu) + '%'; if (m) m.textContent = Math.round(mem) + '%'; if (n) n.textContent = Math.round(net) + ' Mb/s'; if (u) u.textContent = online.toLocaleString('ru'); } setTimeout(hud, 500); setInterval(hud, 1400); }()); </script> <!-- LiveInternet fallback: fires only on pages without the sidebar widget --> <script> (function(d,s){ if (d.getElementById('licntCB0E')) return; // sidebar already handles it var img = new Image(); img.src = 'https://counter.yadro.ru/hit?t29.14;r' + escape(d.referrer) + ((typeof(s)==='undefined') ? '' : ';s'+s.width+'*'+s.height+'*'+(s.colorDepth?s.colorDepth:s.pixelDepth)) + ';u'+escape(d.URL)+';h'+escape(d.title.substring(0,150))+';'+Math.random(); })(document, screen); </script> <!-- /LiveInternet fallback --> <!-- Yandex.Metrika counter --> <script type="text/javascript"> (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window,document,"script","https://mc.yandex.ru/metrika/tag.js","ym"); ym(50511013,"init",{clickmap:true,trackLinks:true,accurateTrackBounce:true,webvisor:true}); </script> <noscript><div><img src="https://mc.yandex.ru/watch/50511013" style="position:absolute;left:-9999px;" alt=""/></div></noscript> <!-- /Yandex.Metrika counter --> </body> </html>