Копируем исходный код без нумерации строк


    Бывает при выводе информации требуется ее декорировать для лучшего восприятия, нередко оформление сопровождается в том числе и текстом. При выделении и копировании этой информации, оформление копировать не нужно, т.к. нужна только сама информация, и желательно в исходном виде. То есть при копировании часть выделенного текста не должна попадать в буфер обмена.
    В моем случае это исходный код, который сопровождается нумерацией строк, так нагляднее и есть возможность сослаться на строку кода. Однако, если мы хотим скопировать часть кода, то он должен копироваться без номера строки.
    Многие highlighter'ы этим грешат, при копировании кода копируют в том числе и номера строк. Выходят из ситуации по разному: либо используют хитрую верстку, при которой возможно несовпадении нумерации со строками (можно заметить на github например), или используют специальную кнопку, которая показывает в отдельном окне код без форматирования. Мне показались эти подходы неудовлетворительными, потому решил найти другое решение.
    В данной заметке я опишу решение, к которому в итоге пришел. Решение, конечно, частное, но может кому то оказаться полезным в решении собственных задач.

    Начнем с того, что кратко пройдемся по возможным вариантам решения.

    Способ первый, очевидный.


    Самый простой и очевидный способ, это сделать нумерованный список, где каждый элемент списка это отдельная строка. Основной плюс у этого подхода: все просто и не нужно задумываться даже о нумерации. Но при этом нельзя управлять форматом нумерации, нельзя позиционировать маркер (номер строки) и вообще как-то его декорировать. К тому же Internet Explorer и Firefox копируют текст вместе с маркером.
    Собственно проблема с копированием перечеркивает возможность использования этого подхода.

    Второй способ, менее очевидный.


    Второе что приходит на ум — использовать генерируемый контент. Можно даже смирится с тем, что в старых IE это не будет работать (на этот случай можно, конечно, сделать fallback). Но беда в том, что Internet Explorer и Opera копируют весь видимый текст, в том числе и генерируемый контент.
    Сложно сказать правы ли разработчики Internet Explorer/Opera или же правы разработчики webkit и Firefox относительно копирования генерируемого контента. Одно можно сказать с уверенностью, что метод не подходит.

    Небольшая ремарка


    В webkit и Firefox поддерживаются такое свойство CSS как user-select (со своими префиксами -webkit-user-select и -moz-user-select), с помощью которых можно запретить выделять текст. В Internet Explorer'е есть html атрибут unselectable. Однако все это лишь предотвращает начало процесса выделения, то есть если текст все же попал в область выделения, то он будет скопирован.
    Более того, все блоки попавшие в область выделения, даже невидимые (display: none; или visibility: hidden;) так же в итоге попадают в буфер обмена.
    Все это весьма печально, так как нет нормального способа определить — какой текст нужно копировать, а какой нет.

    Способ третий, ненормальный.


    Блуждая по интернетам в поисках решения, что-то навело на мысль использовать <input> для вывода декоративного текста. Да-да, поле ввода для вывода текста. Необходимый текст прописываем в значение value, убираем padding, margin, border, background, задаем остальные стили — и никто не скажет что это input. Выделяем наш текст, копируем, вставляем в блокнот — все браузеры ведут себя одинаково, теги <input> с содержимым не вставляются.
    Остается побороть неприятный момент, поле ввода может получать фокус, и нельзя начать выделение текста с нумерации — выделяется текст поля. Но это уже, на самом деле, мелочи.
    Чтобы нельзя было передать полю фокус мышкой в webkit и Firefox есть приятность — это CSS свойство pointer-events, которому можно задать значение none. Для Internet Explorer'а можно добавить для поля атрибут unselectable=«on». С Opera пока никак.
    Чтобы нельзя было передать фокус с клавиатуры (и другими способами) добавляем для <input> атрибуты readonly и tabindex="-1".
    В Firefox и webkit все отлично, но в Internet Explorer нельзя начать выделение начиная с области нумерации, а в Opera по прежнему выделяется текст поля, а не исходный код. Чтобы решить проблему добавим для каждого <input> дополнительный блок, который будет перекрывать <input>. Этот блок должен иметь те же размеры, что и <input>, а так же быть прозрачным. К сожалению (или к счастью), если если блок не имеет фона, то он «простреливается», то есть ведет себя как будто его нет, потому ему нужно добавить background. Можно добавить либо прозрачный gif файл, либо просто путь к несуществующему файлу, например background: url(#); (можно было бы использовать rgba(), но в IE это поддерживается только начиная с 9й версии).

    Вот, в принципе, и все.

    В итоге имеем следующий html для блока с исходным кодом (переводы строк для наглядности, целевой html должен быть без них, иначе можем получить ненужные переводы строк при копировании):
      <div class="code">
        <div class="line">
          <span class="lineContent">
            <input class="lineNumber" value="01" unselectable="on" readonly tabindex="-1" />
            <span class="over"></span>
            .. текст ..
          </span>
        </div>
        <div class="line">
          ...
        </div>
        ...
      </div>
    

    И CSS для этого «безобразия»:
    	   .code
      {
        border: 1px solid #E0E0E0;
        padding: 1px;
        margin: 0 1ex;
        overflow: hidden;
    
        font-family: Consolas, monospace;
        font-size: 100%;
        color: black;
        line-height: 1.4em;
        white-space: pre;   /* важно чтобы сохранялись переносы */
      }
    
      .line
      {
        position: relative; /* для webkit, иначе при начале выделения с нумерации будет
                               выделяться текст с самого начала блока */
        zoom: 1;            /* для IE6/7 */
        white-space: pre;   /* важно чтобы сохранялись переносы в IE7 */
      }
    
      .even
      {
        background: #F8F8F8;
      }
      .odd
      {
        background: #F0F0F0;
      }
    
      .lineNumber
      {
        display: block;
        position: absolute;
        left: 0;
    
        padding: 0 .5ex;
        margin: 0;
        width: 6ex;
        line-height: 1.4em;
        height: 1.4em;
    
        background: none;
        border: none;
        font-family: Consolas, monospace;
        font-size: 100%;
        text-align: right;
        color: #666;
    
          -moz-user-select: none;    /* задаем user-select для Firefox & webkit, чтобы
                                        блок не попадал в область выделения*/
          -webkit-user-select: none;
        user-select: none;
        pointer-events: none;        /* фактически это свойство делает ненужным блок .over, 
                                        но пока оно поддерживается только в Firefox & webkit */
      }
      .over
      {
        display: block;
        position: absolute;
        left: 0;
        width: 7ex;
        height: 1.4em;
        z-index: 1;
        background: url(.);          /* для IE и Opera, иначе блок будет "простреливаться"
                                        (пропускать события мыши нижележащим блокам) */
      }
    
      .lineContent
      {
        margin-left: 7ex;
        padding-left: 2ex;
        border-left: 2px solid #33BB66;
        display: block;
        white-space: pre-wrap;      /* сохраняем форматирование, но оставляем возможность переносить по словам */
      }
    
      .token-string
      {
        color: blue;
      }
      .token-comment 
      {
        color: #008200;
      }
      .token-keyword
      {
        color: #006699;
        font-weight: bold;
      }
    


    Демо


    Демо на примере исходного кода atomjs (надеюсь TheShock не против ;)
    Html-кода много, но он генерируется javascript'ом (свой highlighter).
    Тестировалось под Chrome/Safari, Firefox 3.6, Opera 11, IE7-9 (переключением режимов в IE9 RC).

    На что стоит обратить внимание


    • Решение использует только возможности HTML и CSS, без спец хаков для браузеров.
    • webkit впереди планеты всей, лучше всего работает в браузерах на его движке, можно сказать идеальный сценарий.
    • Для Firefox пробелы в начале строк нужно заменять на &nbsp; иначе при копировании они будут вырезаться.
    • В Opera текст начинает выделятся только если курсор мыши находится непосредственно над текстом. Начать выделение можно и с нумерации, но фактически текст начнет выделяться только когда курсор окажется над текстом.
    • Важна вложенность свойства white-space. В данном случае ключевыми блоками является .code -> .line -> .lineContent, поэтому рабочим будет цепочка pre -> pre -> pre-wrap и, возможно, другие варианты (нужно проверять). От заданых значений зависит как браузер будет трактовать блоки при преобразовании в текст; при определенных значениях браузер может любой блочный элемент (display: block) выделять в отдельную строку текста, при этом могут получиться пустые строки между строками текста или же игнорировать переносы (например, IE7 может копировать весь текст одной строкой).
    • При вставке в редактор, который понимает text/html (например, MS Word), так же вставятся <input> поля. Момент неприятный, но чаще редакторы не понимают text/html, так что переживать не стоит.

    Буду рад замечаниям и советам по улучшению.

    UPDATE
    Найден workaroud чтобы при вставке в Word/Excel не вставлялись поля ввода. Чтобы этого добиться для <input> нужно задать несуществующее значение атрибута type. В таком случае браузеры игнорируют атрибут и поле имеет тип по умолчанию, то есть type=«text», а при копировании (или вставке) не описаны сценарии как поступать с полями неизвестного типа — в итоге поле игнорируется. Так что код можно спокойно вставлять, например, в Word. При этом если код копируется из Chrome/Safari или из IE, то он будет вставлен с раскраской (webkit так же копирует и фон, а IE копирует без фона), что может в ряде случаев может оказаться полезным.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 66

      +10
      Демо на примере исходного кода atomjs (надеюсь TheShock не против ;)

      Только «за»! =)
        0
        Красиво.
          0
          А table не пойдет?
            –1
            table не поможет решить проблему с копированием нумерации это раз. А во-вторых, все давно уже верстается без таблиц.
              +5
              1) решает, но с особенностями
              2) «не модно», не значить «не правильно»
                +3
                А прсмотрщик на code.google.com кажется с этим не согласен.
                  +3
                  Там используется две таблицы, одна для нумерации, другая для строк кода. При определенных событиях, например, изменении размера окна, высота ячеек с нумерацией пересчитывается javascript'ом. В моем решении javascript не используется.
                  +1
                  Ну ну, а отображать текст инпутами — это семантично, да
                    0
                    Предполагается, что в html страницы исходный код в исходном виде (сорри за каламбур), который в момент загрузки с помощью javascript превращается в другой вид, более удобный для чтения, и этот вид не влияет на код если пользователь захочет его скопировать.
                    Так что семантика тут не причем.
                      0
                      Это уже интересный философский вопрос: должен ли быть семантичным и валидным код страницы при загрузке или на всём протяжении просмотра страницы, учитывая скрипты. Я думаю, что должен.
                  +1
                  Вполне table подходит, но нужно выделять для копирования только внутри таблицы. Пример давно реализованного мною варианта:
                  www.pleso.net/ru/publications/2007/10/16/gwt-ibatis/
                    0
                    Ваше решение как раз из разряда «используют хитрую верстку, при которой возможно несовпадении нумерации со строками». По вашей ссылке это наглядно видно, когда строк более 10. То же самое можно сделать и без таблицы с двумя ячейками.
                    Проблемы такого подхода начинаются:
                    1. когда неправильно подобрана высота строки
                    2. когда строка кода переносится (не умещается по ширине)
                      0
                      — строки никогда не переносятся, в примерах кода это не желательно — скопированный код часто не будет работать.
                      — да, небольшое несовпадение возможно, но я еще никогда не наблюдал — у меня четкое совпадение. Возможно там различаются шрифты. Альтернативный вариант — заключение в отдельные ячейки:
                      code.google.com/p/django-evolution/source/browse/trunk/README

                        0
                        Если строки переносить без разрыва строки, то будет все работать
                          +2
                          — Переносить строки хорошо для чтения. При копировании лишних переносов не будет — в том то и соль. Уменьшите окно браузера чтобы некоторые строки стали переноситься и скопируйте/вставьте текст.
                          — У меня явное несовпадение, при том что строк всего ничего (на скриншоте 18)
                            0
                            — вопрос по переносам из розряда предпочтений. Мои IDE не переносят и так значительно более читабельно, струкруриванный код не путается.
                            — хм, браузер, платформа? Возможно елементы подсветки кода и font-weight:bold иногда делают строку выше. Разбиение на ячейки по примеру гугл-кода будет более надежным вариантом.
                              0
                              — Дело не в IDE (в которой текст и не должен переносится автоматически), а как поступать с длинными строками. Или вам нравится когда у блока с кодом, как на скриншоте, есть горизонтальный скроллбар?
                              — win7, проблемы в Chrome 9, Firefox 3.6. В Opera 11.01 используется другой шрифт по умолчанию для pre и вроде ровно. В IE9 RC в режиме IE9 все нормально, в режиме IE7/8 такие же проблемы.
                              Проблема не в разбиении на ячейки, а в том что нужно вычислять высоту каждой строки и выставлять такую же высоту блоку, который содержит номер строки.
                                0
                                — да, собственно, так и было задумано
                                — шрифты, дело мутное. Если же таблица будет разбита на ячейки, выравнивает строки браузер. На то и были введены таблицы
                                  0
                                  — по-моему заставлять пользователя кролить чтобы посмотреть что там правее — это зря. к тому же если строк будет много, то где будет горизонтальный скроллбар?
                                  — будет выравнивать только в том случае если ячейки в одной строке — у Вас это не так
                                    0
                                    — горизонтальный скролбар — это прокрутка всей страницы, он есть всегда.
                                    — и не спорю, написал ведь «если бы». На самом деле думаю будет достаточно поработать с css для pre и стандартизировать отображение между браузерами, и не играться во много-магии.
                                0
                                Да и вообще статья больше про то как вывести текст, который не будет копироваться в дальнейшем. Копирование кода без нумерации лишь частный случай, в качестве примера.
                                  0
                                  тогда заглавие статьи врет )
                      +1
                      Удобно.
                        0
                        Можно использовать css counter-increment.
                          0
                          css counters — это здорово, но это тоже генерируемый контент, и он копируется при выделении Internet Explorer'ом и Oper'ой. К тому же IE поддерживает счетчики если не ошибаюсь только с 8й версии.
                            –2
                            И зачем кому-то с IE
                          +3
                          А я всегда закидываю в Idea, включаю column mode и в две секунды вырезаю ненужные циферки P
                            +3
                            Во многих редакторах (н-р: Notepad++ ) есть «rectangular selection» (alt+shift+«стрелочки» или alt+click) — мегавещь, позволяет копировать, вставлять, удалять по вертикали.
                              0
                              Проверил в Geany. Тоже работает Alt+Shift+стрелки.
                                0
                                В Eclipse и PSPad не работает. :(
                                  0
                                  В Дельфях с рождения есть эта фича.
                                  +2
                                  В редакторе Far Manager'a так можно выделять через Alt. Кстати, скопированная подобным образом область текста вставляется тоже по-особенному. :)
                                  0
                                  Это все прекрасно, но требует дополнительных действий с вашей стороны, что совсем не экономит ваше время ;)
                                  +1
                                  Это скорее раздел «Каскадные Таблицы Стилей», чем «Веб-разработка». Тем более, что в нём где-то уже есть вариант копирования кода.
                                    0
                                    спасибо, работает=)
                                      0
                                      начав или закончив выделение в столбце с номерами, я копирую и номера тоже.
                                      firefox 3.6

                                      причем при вставке они всегда идут перед кодом.
                                        0
                                        ой, сорри, был не прав. ткнул на ссылку с гитхаба. совсем уже голова не варит.
                                        +1
                                        хороший способ для нынешних реалий, но, черт возьми, как же все таки обидно, что браузерные стандарты такие разные…
                                          0
                                          а что если использовать :before?
                                            0
                                            по-моему, самый правильный путь
                                              0
                                              Это генерируемый контент, смотрите второй способ.
                                            0
                                            хм, меня как-то не сильно напрягает после копипаста использовать Alt+Shift+стрелочки. На избавление от нумераций уходит секунд 5.
                                              0
                                              ну а тут о вас дополнительно побеспокоились — мелочь, а приятно ;) Причем, думаю, ее даже не сразу заметишь.
                                                0
                                                согласен, так удобнее, и, наверное, «натуральнее»
                                              +3
                                              Кстати, у Вас на скриншоте горизонт завален.
                                                –1
                                                В хроме такая бага есть(при перетаскивании выделенного текста)

                                                  0
                                                  Нумерация строк за 5 секунд готовым скриптом для vim или sed убирается. Если вдруг наткнётесь на сайт, автор которого решил что это не стоит таких костылей.

                                                  Вот, например:
                                                  $ xsel -o | sed 's/[0-9]\+ //' | xsel -i

                                                  Выделяете (в буфер) текст с нумерацией строк, выполняете (по кнопке в браузере, например) и получаете в буфере тот же текст без нумерации. Нумерацию без извращений использует, например, sprunge.us

                                                  (Решение для Windows дал ancalled выше, хоть оно и требует многомегабайтной коммерческой софтины)
                                                    0
                                                    Кому-то полезным не оказалось, понимаю, не у всех в ОС coreutils и xsel есть. Но зачем делать текст трудночитаемым для тех кому он может пригодиться?
                                                      0
                                                      sed в этом случае заменит все вхождения цифр. не корошо
                                                        0
                                                        То-ли парсер съел «домик», то-ли я его действительно забыл.
                                                      +3
                                                      «КопиПаст» злейший враг программиста :)
                                                        +2
                                                        Точно! Эх, где те времена, когда перепечатывали листинги кода по 30 страниц с учебников!
                                                        +1
                                                        Когда уже в браузерах сделают поддержку
                                                        выделения текста с зажатым Alt(как в MS Word)?
                                                        Вроде ничего сложного в этом нет.
                                                          0
                                                          Поощряете вы копипастеров, совсем ведь обнаглеют, и так спасения от них не найти. Если и правда повсеместно будет что-то подобное, я может быть, меньше стану использовать vim, регекспы… Может быть…
                                                            0
                                                            На самом деле никого не поощряю :) моей целью было дать пользователю возможность скопировать часть кода, без того чтобы думать как это сделать, как убрать нумерацию и т.п.
                                                            0
                                                            Расскажу о своем так и не реализованном способе:
                                                            Делаем спрайт с нумерацией от 0 до 9. Пишем js, который будет выводить наш код, например, так:
                                                            <pre><code>
                                                            <span><i class='one'></i>(function () { </span>
                                                            <span><i class='two'></i> var win = window,</span>
                                                            <span><i class='three'></i>...</span>
                                                            <span><i class='two'><i class=«one»></i></i>...</span>
                                                            </code>
                                                            </pre>
                                                            Пишем стили для строк. Каждый <i> это цифра, показывающая номер строки, т.е. <i class='one'></i> показывает цифру 1, <i class='two'></i> — 2, <i class='two'><i class='one'></i></i> — 21 и т.д.
                                                            По-моему такой подход более правильный, хотя и пример автора понравился, правда исходный код очень нагроможден.
                                                            Мысль такого вывода кода появилась давно, а написать js руки никак не доходят — извиняюсь):
                                                              0
                                                              В CSS будет, например, так:

                                                              i {width:42px;height:18px;display:block;background:url('s.gif') no-repeat}
                                                              .one {background-position:0 0}
                                                              .two {background-position:0 -18px}
                                                              .three {background-position:0 -36px}

                                                              .zero {background-position:0 -162px}
                                                                0
                                                                Такая мысль мелькала, но не понравилось что есть зависимость от изображения, которое еще и не маштабируется
                                                                  0
                                                                  Ну как вариант, можно сделать так. Хотя зачем масштабировать номера строк, если по задаче нужно выделить код листинга без нумерации.
                                                                    0
                                                                    На разных платформах и в разных браузерах по разному выводятся шрифты, к тому же размер шрифта на экране может меняться.
                                                                    На самом деле копирование кода без нумерации — случай частный. Хотелось показать на примере как выводить текст который не будет копироваться, а все поняли как будто копирование без нумерации основная цель статьи :)
                                                              0
                                                              Отличный способ. Спасибо.
                                                                0
                                                                Что-то у меня в IE7 при копировании теряются переводы строк — вставляется все в одну строку :(
                                                                  0
                                                                  Вы правы, упустил момент в ходе многочисленных экспериментов (когда минимизировал стили).
                                                                  Чтобы исправить, нужно для .line задать white-space: pre
                                                                  Обновил статью и демо — теперь должно быть нормально.
                                                                  +1
                                                                  Браузер должен оформлять код. Теги <b>, <ul>, <etc> он же оформляет.

                                                                  Но в текущей ситуации я бы использовал <ol>.
                                                                  Плюсы:
                                                                  • Сами генерируются номера;
                                                                  • Правильно выделяется (даже след пункт);
                                                                  • При переносе строк номера не наезжают и строки переносятся.

                                                                  Минусы конечно тоже есть, и очень даже '—':
                                                                  • Необходимо все пробелы/табы заменять на «&nbsp;».

                                                                  Реализация заключается в обычном заворачивании кода в <code> а потом js пробегаемся и заменяем пробелы/табы, одновременно оборачивая строки в <li></li>
                                                                    0
                                                                    Об этом было в первом способе. Главный минус в том, что Firefox и Internet Explorer копируют нумерацию. Да и к тому же нельзя стилизовать нумерацию.
                                                                      0
                                                                      Не пойму как я читал. Наверное мозг выключал на время прочтения о первом способе :)

                                                                  Only users with full accounts can post comments. Log in, please.