Category Archives: Notepad++

Emmet: balance tag исправляем

Здравствуйте.
На страничке http://docs.emmet.io/actions/match-pair/ у Сергея написано следующее (перевод мой с английского):
********************************************************************************

Balance

“Balance Outward” (Ctrl+D)

“Balance Inward (Shift+Ctrl+D)

Известная балансировка тэгов: поиск границ тэгов и выбор их от текущей позиции каретки. Она может быть расширенной (outward balancing) или суженной (inward balancing) при вызове несколько раз подряд. Не каждый редактор поддерживает одновременно суженную и расширенную балансировку.

Балансировка тэгов Emmet достаточно уникальна. В отличие от остальных, она ищет границы тегов от текущей позиции каретки, а не от начала документа. Это означает, что она может работать не только в HTML документах.
********************************************************************************

Да, классная штука, правда, в реальной жизни нас скорее интересует текущий тег – просмотр границ и переход в начало конец этого тэга (для этого у Emmet‘а есть действие matching pair).

Но если она (балансировка) есть, хотелось бы использовать её на полную мощь. Так как я добавил поддержку Emmet для редактора AkelPad (http://akelpad.sourceforge.net/forum/viewtopic.php?p=30266#30266), мне захотелось подробнее разобраться в вопросе, а именно меня задели слова, что “Не каждый редактор поддерживает одновременно суженную и расширенную балансировку.“.

Экспериментируя с Emmet‘ом в AkelPad‘e, я заметил недоделанность эту (отсутствие балансировки вглубь, в частности), и решил посмотреть, что можно сделать.

Итак, нас интересует такой участок кода в файле emmet-app.js (я добавлял поддержку Emmet’а для AkelPad’а, используя плагин для Komodo Edit‘а, а потом проанализировал подобный файл от плагина к Notepad++):

/**
 * HTML pair matching (balancing) actions
 * @constructor
 * @memberOf __matchPairActionDefine
 * @param {Function} require
 * @param {Underscore} _
 */
emmet.exec(function(require, _) {
	/** @type emmet.actions */
	var actions = require('actions');
	var matcher = require('htmlMatcher');
	var lastMatch = null;
 
	/**
	 * Find and select HTML tag pair
	 * @param {IEmmetEditor} editor Editor instance
	 * @param {String} direction Direction of pair matching: 'in' or 'out'. 
	 * Default is 'out'
	 */
	function matchPair(editor, direction) {
		direction = String((direction || 'out').toLowerCase());
		var info = require('editorUtils').outputInfo(editor);
 
		var range = require('range');
		/** @type Range */
		var sel = range.create(editor.getSelectionRange());
		var content = info.content;
 
		// validate previous match
		if (lastMatch && !lastMatch.range.equal(sel)) {
			lastMatch = null;
		}
 
		if (lastMatch && sel.length()) {
			if (direction == 'in') {
				// user has previously selected tag and wants to move inward
				if (lastMatch.type == 'tag' && !lastMatch.close) {
					// unary tag was selected, can't move inward
					return false;
				} else {
					if (lastMatch.range.equal(lastMatch.outerRange)) {
						lastMatch.range = lastMatch.innerRange;
					} else {
						var narrowed = require('utils').narrowToNonSpace(content, lastMatch.innerRange);
						lastMatch = matcher.find(content, narrowed.start + 1);
						if (lastMatch && lastMatch.range.equal(sel) && lastMatch.outerRange.equal(sel)) {
							lastMatch.range = lastMatch.innerRange;
						}
					}
				}
			} else {
				if (
						!lastMatch.innerRange.equal(lastMatch.outerRange) 
						&& lastMatch.range.equal(lastMatch.innerRange) 
						&& sel.equal(lastMatch.range)) {
					lastMatch.range = lastMatch.outerRange;
				} else {
					lastMatch = matcher.find(content, sel.start);
					if (lastMatch && lastMatch.range.equal(sel) && lastMatch.innerRange.equal(sel)) {
						lastMatch.range = lastMatch.outerRange;
					}
				}
			}
		} else {
			lastMatch = matcher.find(content, sel.start);
		}
 
		if (lastMatch && !lastMatch.range.equal(sel)) {
			editor.createSelection(lastMatch.range.start, lastMatch.range.end);
			return true;
		}
 
		lastMatch = null;
		return false;
	}
 
	...
 
}

при беглом изучении этого кода, можно выделить такую фразу:

		// validate previous match
		if (lastMatch && !lastMatch.range.equal(sel)) {
			lastMatch = null;
		}

Вон оно что. Тут у нас статическая переменная lastMatch. Дело в том, что если JavaScript скрипт каждый раз подключается заново, статические переменные теряют смысл, так как они каждый раз заново инициализируются.

В браузерах после загрузки страницы все скрипты остаются в памяти и статические переменные работают как надо. В Notepad++ также подключение скрипта emmet-app.js происходит только один раз при первом вызове (используется Python-wrapper для JavaScript), поэтому там также всё работает более-менее нормально.

А в AkelPad‘е каждый раз мы запускаем скрипт заново, то есть он не висит в памяти, занимая пространство и время редактора, поэтому в нашем случае эта статическая переменная бесполезна.

Штука такая, что на самом деле от статической составляющей lastMatch можно уйти. Предположив, что если у нас что-то выбрано, lastMatch уже отработал и нам надо искать дальше.

То есть мы в любом случае ищем как сначала, переставив местами части.

Вот что в итоге получается:

/**
 * HTML pair matching (balancing) actions
 * @constructor
 * @memberOf __matchPairActionDefine
 * @param {Function} require
 * @param {Underscore} _
 */
emmet.exec(function(require, _) {
	/** @type emmet.actions */
	var actions = require('actions');
	var matcher = require('htmlMatcher');
	var lastMatch = null;
 
	/**
	 * Find and select HTML tag pair
	 * @param {IEmmetEditor} editor Editor instance
	 * @param {String} direction Direction of pair matching: 'in' or 'out'. 
	 * Default is 'out'
	 */
	function matchPair(editor, direction) {
		direction = String((direction || 'out').toLowerCase());
		var info = require('editorUtils').outputInfo(editor);
 
		var range = require('range');
		/** @type Range */
		var sel = range.create(editor.getSelectionRange());
		var content = info.content;
 
		// we do not have static lastMatch here
		lastMatch = matcher.find(content, sel.start);
		// try for in balancing
		if(!lastMatch)
			lastMatch = matcher.find(content, sel.start + 1);
 
		if (lastMatch && sel.length()) {
			if (direction == 'in') {
				// user has previously selected tag and wants to move inward
				if (lastMatch.type == 'tag' && !lastMatch.close) {
					// unary tag was selected, can't move inward
					return false;
				} else {
					if (lastMatch.range.equal(lastMatch.outerRange)) {
						lastMatch.range = lastMatch.innerRange;
					} else {
						var narrowed = require('utils').narrowToNonSpace(content, lastMatch.innerRange);
						lastMatch = matcher.find(content, narrowed.start + 1);
						if (lastMatch && lastMatch.range.equal(sel) && lastMatch.outerRange.equal(sel)) {
							lastMatch.range = lastMatch.innerRange;
						}
					}
				}
			} else {
				if (
						!lastMatch.innerRange.equal(lastMatch.outerRange) 
						&& lastMatch.range.equal(lastMatch.innerRange) 
						&& sel.equal(lastMatch.range)) {
					lastMatch.range = lastMatch.outerRange;
				} else {
					lastMatch = matcher.find(content, sel.start);
					if (lastMatch && lastMatch.range.equal(sel) && lastMatch.innerRange.equal(sel)) {
						lastMatch.range = lastMatch.outerRange;
					}
				}
			}
		}
 
		if (lastMatch && !lastMatch.range.equal(sel)) {
			editor.createSelection(lastMatch.range.start, lastMatch.range.end);
			return true;
		}
 
		return false;
	}
 
	...
 
}

Для balancing inward я добавил такую фичу:

		// try for in balancing
		if(!lastMatch)
			lastMatch = matcher.find(content, sel.start + 1);

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

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

Но сам движок Сергея по поиску HTML/XML тэгов (matcher.find) нуждается в доработке, так он вне HTML может спотыкаться при использовании экранированных кавычек и ряда других случаях. Но этим, насколько я мог заметить, грешат практически все наверное подобные алгоритмы, так как найти matching tag в определённых случаях бывает довольно тяжело.

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

Благодарю Вас за чтение и Господа за еду.

Notepad++, апгрейд до версии 6.5 и патч html.xml из plugins/APIs

Я уже давний поклонник этой программы – Notepad++ (http://notepad-plus-plus.org/)

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

Вот вышла версия 6.5 недавно – там добавлись фичи – автозакрытие открывающих тагов (скобок и т.п.) плюс наконец-то добавились парсеры для языков PHP, Perl и др. в function list

Буквально с сайта программы – Enhancement of function list: new reload button, user defined languages supported, new parsers (php, perl, xml, batch, ini and nsis).

Класс! Это мне нравится.

И DSpellcheck – то то о чём я много лет мечтал и хотел сам сделать, но голова не доходила, он улучшился уже в версии 6.4.3 с которой я сегодня проапргрйдился до 6.5 – щелчок правой кнопкой мыши на подчёркнутым им словом сразу показывает возможные варианты вместе с другим контекстным меню (как в нормальных прогах), раньше для этого появлялся отдельный значок при наведении на такое слово, и варианты показывались при щелчке на этом значке, а не через контекстное меню, что немного напрягало – непривычно было, но я с этим смирился, а теперь и это доделали.

Для DSpellCheck поставил мульти- en-ru словарик, чтобы на страничке нормально проверялся не только русский текст, но и английский. Самое прекрасное – там есть встроенный даунлоадер словариков, выбираешь нужный и через минуту он готов, также есть возможность использовать несколько словарей одновременно – незаменимая для всех не-англоязычных имхо вещь, так как большинство компьютерных терминов на английском, хочется, чтобы они также проверялись корректно.

Ребята, вы – супер!

function list конечно я ещё не проверил, но проверю обязательно!

ДА, но в этой проге давно гуляет странный баг, ну не в самой проге конечно, а в её файлах автозавершения для разных языков, которые находятся в папке plugins/APIs

Так вот, я заметил, что в моих HTML не работает autocompletion. Что за? Неужели никому не нужно?

Итак, я залазюю *верно слово написал? :-)* в plugins/APIs открываю файл html.xml и вижу там примерно такое (я сократил пару списков, добавив свои [..]:

<?xml version="1.0" encoding="Windows-1252" ?>
<NotepadPlus>
<![CDATA[
Name: Douglas Pereira / Nickname: Deek
e-Mail: douglas_pereira@outlook.com
]]>
<!--HyperText Mark-up Language 5-->
<AutoComplete>
<!--Elements-->
<KeyWord name="!DOCTYPE" />
<KeyWord name="a" />
<KeyWord name="abbr" />
<KeyWord name="address" />
<KeyWord name="area" />
<KeyWord name="article" />
[..]
<KeyWord name="wrap" />
<KeyWord name="xml:lang" />
<KeyWord name="xmlns" />
<!--Global Attributes-->
<KeyWord name="accesskey" />
<KeyWord name="class" />
<KeyWord name="contenteditable" />
[..]
<KeyWord name="spellcheck" />
<KeyWord name="title" />
<!--Window Events-->
<KeyWord name="onafterprint" />
<KeyWord name="onbeforeprint" />
[..]
<KeyWord name="onundo" />
<KeyWord name="onunload" />
<!--Form Events-->
<KeyWord name="onblur" />
<KeyWord name="onchange" />
<KeyWord name="oncontextmenu" />
[..]
<KeyWord name="onselect" />
<KeyWord name="onsubmit" />
<!--Keyboard Events-->
<KeyWord name="onkeydown" />
<KeyWord name="onkeypress" />
<KeyWord name="onkeyup" />
<!--Mouse Events-->
<KeyWord name="onclick" />
<KeyWord name="ondblclick" />
<KeyWord name="ondrag" />
 
[..]

и т.д. и т.п.

Ну Вы поняли идею? То есть автор этого autocompletion’а (некий Douglas Pereira по кличке Deek) для HTML умно (по его мнению) разделил типы HTML свойств, как-то – элементы, атрибуты, события и т.п. и с соотв. комментарями их поместил в этот файл.

Спасибо ему конечно за сам этот файл… но…

…Но мы тут не учебник по HTML пишем, в этот файл вообще никто не должен заглядывать, кроме программы Notepad++ 🙂

ОК. И что же? Но…

…Но если мы прочитаем правила для составления файлов-автозавершений (здесь хотя бы – https://sourceforge.net/apps/mediawiki/notepad-plus/index.php?title=Auto_Completion) то узнаем, что для корректной работы такого файла нужно сортировать все items (никак не могу найти подходящего русского слова для этого ёмкого английского слова – item, то же самое касается супер-английского слова issue) в строгом алфавитном порядке, иначе такой файл не валидный, то есть не будет воспринматься Notepadd++ для автозавершения.

Почему этот corrupted html.xml в папке plugins/APIs успешно гуляет из версии в версию Notepad++ дьля меня загадка, единственный ответ на который – все причастные используют другие редакторы для правки html файлов, только я один использую Notepad++ для таких целей.

Наконец мне это надоело, я написал простенький парсер, даже по-моему средствами поиска – замены самого Notepadd++ (используя встроенный недо-RegExp), выстроил все items в строгом алфавитном порядке, да и добавил туда ещё items from css.xml отсюда же (из папки plugins/APIs) ведь иногда приходится стайлить элементы напрямую (через атрибут style), хоть я и отхожу от этого постепенно, и …

Вот как это теперь примерно выглядит:

<?xml version="1.0" encoding="Windows-1252" ?>
<NotepadPlus>
	<AutoComplete>
		<KeyWord name="a" />
		<KeyWord name="abbr" />
		<KeyWord name="above" />
		<KeyWord name="absolute" />
		<KeyWord name="accept" />
		<KeyWord name="accept-charset" />
		<KeyWord name="accesskey" />
		<KeyWord name="action" />
		<KeyWord name="address" />
		<KeyWord name="alt" />
		<KeyWord name="always" />
		<KeyWord name="aqua" />
		<KeyWord name="area" />
		<KeyWord name="armenian" />
		<KeyWord name="article" />
		<KeyWord name="aside" />
		<KeyWord name="async" />
		<KeyWord name="attr" />
		<KeyWord name="audio" />
		<KeyWord name="auto" />
		<KeyWord name="autocomplete" />
		<KeyWord name="autofocus" />
		<KeyWord name="autoplay" />
		<KeyWord name="avoid" />
		<KeyWord name="azimuth" />
		<KeyWord name="b" />
		<KeyWord name="background" />
[..]
		<KeyWord name="width" />
		<KeyWord name="word-spacing" />
		<KeyWord name="wrap" />
		<KeyWord name="x-fast" />
		<KeyWord name="x-high" />
		<KeyWord name="x-loud" />
		<KeyWord name="x-low" />
		<KeyWord name="x-slow" />
		<KeyWord name="x-soft" />
		<KeyWord name="xml:lang" />
		<KeyWord name="xmlns" />
		<KeyWord name="yellow" />
		<KeyWord name="z-index" />
	</AutoComplete>
</NotepadPlus>

теперь наслаждаюсь полным автозаверешением для своих HTML файлов! 🙂

Так же выложил этот файлик как патч для новых версий Notepadd++ – пусть наслаждаются, мне не жалко.

Патчик находится здесь, откуда можно скачать обновлённый корректный html.xml –

https://sourceforge.net/p/notepad-plus/patches/535/

Да, Notepad++ – это зверь, это тебе не WordPress )))