GrabDuck

Плагин к Sublime Text для публикации статей на Хабр

:

Для написания статей на Хабр я пользуюсь текстовым редактором Sublime Text. Почему это хороший редактор на Хабре уже много раз писали (например тут). Есть, однако, при написании статьи момент, когда её нужно переносить для публикации на Хабр, ну, знаете: Хабр->Добавить пост->Название, хабы, текст (Ctrl+C/Ctrl+V), метки, предпосмотр. В этот момент оказывается, что как-то текст с картинками сверстался на Хабре некрасиво, начинаются правки. Править в браузере? Неудобно и небезопасно. Править в Sublime и постоянно копипастить? Неудобно и надоедает.

Поэтому я сделал для себя небольшой плагин к Sublime, который умеет взаимодействовать с Хромом и Хабром, перекидывая по хоткею в Sublime написанный текст в редактор на странице создания нового топика с автоматическим нажиманием кнопки «Предпросмотр». Это позволяет писать статью в Sublime и в одно нажатие видеть результат её отображения на Хабре.

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

Преамбула


Плагин под Sublime Text 3 и Google Chrome. С минимальными правками он может быть адаптирован под Sublime Text 2 и с чуть большими — под Firefox. Просто мне это не очень интересно, а кому хочется — ну, вы в курсе, как работает кнопочка Fork на GitHub.

Создание нового плагина для Sublime Text


Нет ничего проще. Запускаем Sublime Text, идём в меню — " Tools -> New Plugin…" и получаем новый текстовый файл со следующим содержанием:
import sublime, sublime_plugin

class ExampleCommand(sublime_plugin.TextCommand):
	def run(self, edit):
		self.view.insert(edit, 0, "Hello, World!")

Нажимаем Ctrl+S и видим, что диалог сохранения файла предлагает нам сохранить его в папку User в профиле пользователя. Не соглашаемся, переходим на один уровень вверх (в папку Packages), создаем здесь папку SublimeHabrPlugin и сохраняем наш файл в эту папку с именем SublimeHabrPlugin.py. Прелесть Sublime Text в том, что ничего не надо билдить, подключать, перезапускать. В момент сохранения файла автоматически произойдет его подгрузка в редактор. Проверить это можно открыв консоль Sublime Text (нажмите Ctrl+~), в ней должно быть написано что-то типа

reloading plugin HabrPlugin.HabrPlugin

теперь можно выполнить в этой же консоли команду
view.run_command('example')

и убедиться, что в текст текущего файла добавилась фраза «Hello, World!». Название команды «example» получилось из имени класса ExampleCommand путем отбрасывания суффикса «Command» и преобразования оставшейся части из CamelCase в underscore_case. Т.е. для добавления команды «habr» наш класс должен называться HabrCommand.

Подготовка Хрома


Каким же образом мы будем перекидывать наш текст из Sublime Text в Google Chrome, да и ещё не просто «куда-нибудь», а именно в поле редактора текста на странице нового топика Хабра, с последующим нажатием кнопки «Предпросмотр»? Здесь нам на помощь приходит такая штука, как Chrome Remote debugging protocol. Это протокол, который позволяет подключится к Хрому, получить список открытых вкладок и полный контроль над каждой из них (модификацию контента, подписку на события, выполнениe JavaScript, установка breakpoint'ов) — т.е. фактически всё то, что позволяют делать «инструменты разработчика», которые по F12 открываются. Я открою даже более страшный секрет — сами эти инструменты именно по этому протоколу и работают, что даёт возможность подключится из одного Хрома к отладке страниц в другом Хроме, на удалённой машине или в мобильной версии.

Для разрешения внешних подключений нам нужно запускать Хром с параметром командной строки

chrome.exe --remote-debugging-port=9222

Лично я сделал себе ярлычок «Статья на Хабр» на рабочем столе, запускающий хром вот так:

«C:\Program Files (x86)\Google\Chrome\Application\chrome.exe» --remote-debugging-port=9222 habrahabr.ru/topic/add

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

Пишем сам плагин


Идея плагина уже понятна. Мы будем брать весь текст текущего файла, подключаться к Хрому, получать список вкладок, коннектится к вкладке с открытой страницей Хабра по протоколу WebSockets и выполнять Javascript, который вставит текст в нужное поле и нажмет кнопку «Предпросмотр». Итак, идём по шагам.
Как взять весь текст текущего файла
text = self.view.substr(sublime.Region(0, self.view.size()))
Как подключиться к Хрому и получить список вкладок

Нам нужно подключиться по HTTP к локальной машине на порт 9222, получить JSON с метаданными, распарсить его и получить из него ссылку на строку подключения к вкладке с Хабром по протоколу WebSockets.
import json
import urllib
import sys

def download_url_to_string(url):
	request = urllib.request.Request(url)
	response = urllib.request.urlopen(request)
	html = response.read()
	return html

info_about_tabs = download_url_to_string('http://localhost:9222/json')
		info_about_tabs = info_about_tabs.decode("utf-8")
		decoded_data = json.loads(info_about_tabs)
		first_tab_websocket_url = decoded_data[0]['webSocketDebuggerUrl']
		print(first_tab_websocket_url)

Если этот код запустить — в консоли будет выведено что-то типа
ws://localhost:9222/devtools/page/FD3D0027-0D2D-4DD0-AD1C-156FCA561F7E

Это и есть URL для подключения к отладке вкладки.

Подключение к отладке вкладки по WebSocket


Протокол WebSocket здесь выбран Хромом из-за его удобства в двухсторонних коммуникациях — сервер может посылать клиенту нотификации о происходящих событиях немедленно, не дожидаясь его следующего запроса. Поддержка WebSocket, в отличии от HTTP, не входит в стандартную библиотеку Python, поэтому нам придётся взять стороннюю библиотеку. Я нашел вот эту, она вполне справляется с поставленными задачами.

Итак, подключаемся к нужной вкладке и выполняем, для начала, в её контексте простой Javascript:

import json
import urllib
import SublimeHabrPlugin.websocket


def send_to_socket(socket, data):
	socket.send(data)

def send_script_to_socket(socket, script):
	send_to_socket(socket, '{"id": 1, "method": "Runtime.evaluate", "params": { "expression": "' + script + '", "returnByValue": false}}')

def on_open(ws):
	print('Websocket open')
	send_script_to_socket(ws, 'alert(\'Гав!\')')

def on_message(ws, message):
	decoded_message = json.loads(message)
	print(decoded_message)
	ws.close()

def connect_to_websocket(url):
	res = SublimeHabrPlugin.websocket.WebSocketApp(url, on_message = on_message)
	res.on_open = on_open
	return res

first_tab_websocket = connect_to_websocket(first_tab_websocket_url)
first_tab_websocket.run_forever()

JavaScript, который вставляет текст в редактор на Хабре и нажимает кнопку «Предпросмотр»
Ну, это совсем просто:

document.getElementById('text_textarea').innerHTML = 'text';
document.getElementsByName('preview')[0].click();

Ну и теперь всё это вместе.

import sublime, sublime_plugin
import json
import urllib
import SublimeHabrPlugin.websocket
import sys
import base64

def download_url_to_string(url):
	request = urllib.request.Request(url)
	response = urllib.request.urlopen(request)
	html = response.read()
	return html

def send_to_socket(socket, data):
	socket.send(data)

def send_script_to_socket(socket, script):
	send_to_socket(socket, '{"id": 1, "method": "Runtime.evaluate", "params": { "expression": "' + script + '", "returnByValue": false}}')

def on_open(ws):
	text_to_send = str(base64.b64encode(bytes(ws.text, "utf-8")), encoding='UTF-8')
	send_script_to_socket(ws, 'document.getElementById(\'text_textarea\').innerHTML = atob(\'' + text_to_send + '\');document.getElementsByName(\'preview\')[0].click();')

def on_message(ws, message):
	decoded_message = json.loads(message)
	ws.close()

def connect_to_websocket(url):
	res = SublimeHabrPlugin.websocket.WebSocketApp(url, on_message = on_message)
	res.on_open = on_open
	return res

class HabrCommand(sublime_plugin.TextCommand):
	def run(self, edit):
		text = self.view.substr(sublime.Region(0, self.view.size()))
		info_about_tabs = download_url_to_string('http://localhost:9222/json')
		info_about_tabs = info_about_tabs.decode("utf-8")
		decoded_data = json.loads(info_about_tabs)
		first_tab_websocket_url = decoded_data[0]['webSocketDebuggerUrl']
		print(first_tab_websocket_url)
		first_tab_websocket = connect_to_websocket(first_tab_websocket_url)
		first_tab_websocket.text = text
		first_tab_websocket.run_forever()

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

Вешаем выполнение команды «habr» на хоткей в Sublime Text


Открываем «Preferences -> Key Bindings — User». Дописываем туда:
{ "keys": ["alt+shift+h"], "command": "habr" }

Результат

Запускаем Хром, открываем Sublime Text, пишем «Test», нажимаем alt+shift+h:

Проект на GitHub