Вложенные ссылки

Котики играют во вложенные ссылки

Проблема

В спецификации HTML есть множество разнообразных запретов. Обоснованность многих из них очень хочется оспорить. Один из примеров, с которым мне приходится сталкиваться чаще всего, — вложенные ссылки.

Спецификация прямо запрещает вкладывать одну ссылку в другую:

The a element

[…]

Content model: transparent, but there must be no interactive content descendant.

Если так сделать, то парсер браузера вас не поймёт и, как только встретит открывающий тег вложенной ссылки, закроет родительскую ссылку перед собой:

<a href="#Foo">
    Foo
    <a href="#Bar">
        Bar
    </a>
    Baz
</a>

в глазах браузера станет чем-то таким —

<a href="#Foo">
    Foo
    </a><a href="#Bar">
        Bar
    </a>
    Baz

Живой пример:

[demo:nested-links-broken]

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

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

Перепробовав в голове все варианты, я понял, что для моей задачи может подойти только полная эмуляция на JS — средствами чистого CSS достичь того, что мне требовалось, оказалось невозможно. Но все мы знаем, что эмулировать нативные элементы на JS — одно из самых неблагодарных дел. И я решил поэкспериментировать ещё.

И — нашёл решение. При этом, чисто HTML-решение, дающее возможность вкладывать любое количество нативных ссылок друг в друга.

Решение

[demo:nested-links-simple]

<a href="#a">
    Foo
    <object type="lol/wut">
        <a href="#b">
            Bar
        </a>
    </object>
    Baz
</a>

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

Почему это работает

Что такое, в теории, объекты? Это некие внешние сущности, тип которых задаётся атрибутом type, а содержимое/ссылка на объект задаётся атрибутом data. Содержимое же между открывающим и закрывающим тегом object на самом деле является фолбеком, и должно отображаться в том случае, если браузер не способен по какой-либо причине отобразить соответствующее содержимое. Например, если в браузере не установлен определённый плагин.

Если прописать в атрибут type неизвестный природе MIME-тип, то браузер сразу же перейдёт к отображению фолбека. Но он это сделает (На самом деле, см. дополнение к статье) и в том случае, если мы вообще не зададим ни один из «обязательных» атрибутов.

Таким образом, обрамляя любой HTML в такой безатрибутный <object>, мы получаем просто элемент-враппер с содержимым. Но враппер с очень необычным свойством: любое его содержимое будет верно распознано парсером вне зависимости от того, какой у объекта был контекст. Используя это свойство, мы можем, наконец, вложить ссылку в другую ссылку.

Предполагаю, что такое поведение объектов обусловлено тем, что эти фолбеки используются чаще всего для того, чтобы показать ссылку вида «у вас не установлен наш замечательный плагин, скачайте же его!» для всяких объектов (например, флеш-роликов). При этом многие разработчики наверняка хотели использовать объект как обычный контент, то есть вкладывать его в ссылку, в параграф, в заголовок — да куда угодно. И тут браузерам пришлось перестраховаться и разрешить вкладывать в объект всё что угодно, чтобы при копипасте кода объекта откуда-либо и вставке его в содержимое страницы у авторов ничего не сломалось.

Поддержка браузерами

В некоторых браузерах такое поведение появилось не сразу.

  • Internet Explorer поддерживает вложенные объекты только с девятой версии.

  • Firefox — с четвёртой.

  • Opera — с как минимум девятой (может, и с более ранней — я не стал углубляться ещё дальше).

  • Вебкиты — все, что проверял, Сафари — точно с 5.1, Хром — с 14, дальше не пошёл.

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

Фолбек для IE

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

<a href="…">
    текст основной ссылки…
    <object>
        <!--[if gte IE 9]><!--><a href="…"><!--<![endif]-->
            content of the nested link…
        <!--[if gte IE 9]><!--></a><!--<![endif]-->
    </object>
</a>

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

Это валидно?

Нет, ни разу. Это не валидно, потому что у объекта нет ни одного из требуемых спецификацией атрибутов. Можно было бы указать какой-либо валидный mime-тип вроде type="lol/wut", и сам по себе такой объект стал бы валидным, но, как только мы вложим в него ссылку, валидатор начнёт на эту вложенность ругаться.

Очевидно, что валидатор — давно не показатель чего-либо, кроме формального соответствия кода спецификациям. В данном случае само подобное использование ссылки внутри объекта внутри ссылки может быть совершенно оправданным (примеры типа «скачать плагин»), поэтому он не должен вызывать ошибку валидации.

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

Примеры использования

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

  • Выдержки из статей, когда в сниппете, содержащем первые несколько предложений, могут оказаться ссылки, которые не захочется вырезать при создании сниппета.

  • Сноски и вложенные термины, которые могут оказаться внутри ссылок.

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

* * *

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

Например, не так давно появилась возможность использовать элементы details и figure. Но, только подумайте: по спецификации они могут находиться только в блочном контексте. У вас не может быть иллюстрации с подписью, привязанной к определённому слову в абзаце, а также не может быть расширенного описания какого-либо слова или предложения (скажем, для сносок; какие бы вы выбрали теги для сносок внутри абзацев?)

Трюк с <object> решает все эти проблемы. Вопрос только в том, будет ли его использование оправданным. Лично я считаю, что многие запреты в спецификациях бессмысленны, и возможность обойти их при разумной аргументации бесценна.

Update from 2015-03-05

Владимир Родкин обнаружил, что плагин Flashblock для Firefox убирает со страниц «сломанные объекты», и он считает таковыми безатрибутные <object>. Добавление неизвестного природе mime-типа вроде type="lol/wut" решает эту проблему и ФФ начинает правильно воспринимать объект.

Опубликовано 10 февраля 2015 в Экспериментах . Спасибо Fev за иллюстрацию.