Table of Content with Book-like Page Numbers

by denvdmj

HTML

<h1>Жемчужины программирования</h1>
<author>Джон Бентли</author>

<h2>Содержание</h2>
<ul class="toc">
  <li page="15">Предисловие
    <ul>
      <li page="15">О книге</li>
      <li page="16">Программы</li>
      <li page="17">Читателям первого издания</li>
      <li page="17">Благодарности к первому изданию</li>
      <li page="18">Благодарности ко второму изданию</li>
      <li page="18">От издательства</li>
    </ul>
  </li>

  <li page="19">Часть 1. Предварительные сведения
    <ul>
      <li page="21">Глава 1. Как расколоть орешек
        <ul>
          <li page="21">1.1. Дружеский разговор</li>
          <li page="22">1.2. Точная постановка задачи</li>
          <li page="23">1.3. Разработка программы</li>
          <li page="24">1.4. Набросок решения</li>
          <li page="25">1.5. Основные принципы</li>
          <li page="26">1.6. Задачи</li>
          <li page="28">1.7. Дополнительная литература</li>
        </ul>
      </li>

      <li page="29">Глава 2. Ага! Алгоритмы
        <ul>
          <li page="29">2.1. Три задачи</li>
          <li page="30">2.2. Вездесущий двоичный поиск</li>
          <li page="32">2.3. Мощь элементарного</li>
          <li page="34">2.4. Соберем все вместе: сортировка</li>
          <li page="35">2.5. Принципы
            <ul>
              <li page="35">Сортировка</li>
              <li page="35">Двоичный поиск</li>
              <li page="35">Сигнатуры</li>
              <li page="84">Постановка задачи</li>
              <li page="36">Перспективы для программиста</li>
            </ul>
          </li>
          <li page="36">2.6. Задачи</li>
          <li page="38">2.7. Дополнительная литература</li>
          <li page="38">2.8. Реализация поиска анаграмм</li>
        </ul>
      </li>

      <li page="41">Глава 3. Программы и структуры данных
        <ul>
          <li page="41">3.1. Программа обработки результатов опроса</li>
          <li page="43">3.2. Обработка шаблонных писем</li>
          <li page="45">3.3....

CSS

@import url('https://fonts.googleapis.com/css?family=Play|Roboto+Condensed');

:root {
  font: 300 16px/1.3 "Roboto", sans-serif;
  font-stretch: ultra-condensed;
  text-align: left;
  cursor: default;
}

body {
  margin: auto;
  padding: 1em 2em 4em 2em;
  min-width: 18em;
  max-width: 42em;
  background: #fff;
}

h1, h2 {
  font-family: Play, sans-serif;
  font-weight: 400;
  padding: 0;
  margin: 2em 0 .5em -1px;
  line-height: 1.3;
  word-spacing: .1em;
  letter-spacing: .15em;
  color: #222;
  clear: both;
}

body h1:first-child {
  font-size: 190%;
  margin: 1em 0 .5em -1px;
}

h1 {
  border-bottom: 1px solid #ddd;
  clear: both;
  color: #000;
  font-size: 230%;
  letter-spacing: .1em;
  margin: 0 0 .3em 0;
  padding: 0 0 .3em 0;
}

h2 {
  font-size: 170%;
}

ul li { list-style-type: square; }
ul li li { list-style-type: disc; }

ul.toc {
  border-left: 1em solid #EEE;
}

ul.toc ul {
  margin: 0;
  padding: .5em 0 1em 3em;
}

ul.toc li          { color: #A00; text-shadow: #000 0 0 0; }
ul.toc li li       { color: #0A0; }
ul.toc li li li    { color: #77C; text-shadow: #fff 0 0 0; }
ul.toc li li li li { color: #AA5; }

ul.toc a {
  text-decoration: none;
  color: #444;
}

ul.toc a:hover {
  color: blue;
  color: #A00;
  text-decoration: underline;
}

/* page numbers */

li {
  position: relative;
}
li > a {
  background: #fff;
  display: inline-block;
  padding-right: .5em;
}
li:before {
  text-align: right;
  content: ' ';
  float: right;
  width: 100%;
  height: calc(1em - 1px);
  margin-bottom: -1em;
  right: 0;
  border-bottom: 1px dashed #000;
}
li:after {
  content: attr(page);
  position: absolute;
  right: 0;
  top: 0;
  padding-left: .3em;
  background: #fff;
}

JavaScript

const formatAsID = (node, format = '%s') => 
  format.replace(/%s/, 
  	node.textContent.trim().replace(/[^\wА-Яа-я.]+/g, '-')
  )
;

const replaceWithLink = (node, url) => {
  let a = document.createElement('a');
  node.replaceWith(a);
  a.href = url;
  a.append(node);
};

for (let chapLI of document.querySelectorAll('ul.toc > li')) {
  let textNode = chapLI.firstChild;
  let chapURL = formatAsID(textNode, '%s.html');
  replaceWithLink(textNode, chapURL);
  for (let subChapLI of chapLI.querySelectorAll('ul.toc > li li')) {
    let textNode = subChapLI.firstChild;
    replaceWithLink(textNode, formatAsID(textNode, chapURL + '#%s'));
  }
}

for (let header of document.querySelectorAll('h1,h2,h3,h4,h5,h6')) {
  let textNode = header.firstChild;
  header.id = formatAsID(textNode);
}