Как вставить цифры в элементы заголовка

Я создал плагин JavaScript, который превращал заголовки в стиль документации (также известный как иерархические нумерованные структуры или многоуровневые нумерованные структуры).

Эти стили вы часто видите в юридических документах, технических руководствах. Например:

1. Main title
1.1. Sub-title
1.1.1. Sub-sub-title

Теперь я нашел время провести рефакторинг и сделать плагин работающим и надежным. Но у меня действительно проблемы с вводом и увеличением чисел.

Это текущий код, который у меня есть:

const scopeHeadingElements = scope.querySelectorAll(levelsRange);
const currentNumbers = {
  h1: headingNumbers.h1,
  h2: headingNumbers.h2,
  h3: headingNumbers.h3,
  h4: headingNumbers.h4,
  h5: headingNumbers.h5,
  h6: headingNumbers.h6
};
scopeHeadingElements.forEach((heading) => {
  const tagName = heading.tagName;
  const headingLevel = parseInt(tagName.substring(1));
  let numberText = '';
  currentNumbers['h' + (headingLevel + 1)]++;
  for (var levelNumber = 1; levelNumber <= 6; levelNumber++) {
    if (levelNumber <= headingLevel) {
      numberText += currentNumbers['h' + levelNumber] + options.separator;
    } else {
      continue;
    }
  }
  heading.innerHTML = `${numberText} ${heading.innerHTML}`;
});

scopeHeadingElements возвращает любой из элементов H1-H6 в DOM в зависимости от того, что такое levelRange. Например, это могут быть все элементы от H1 до H6 или они могут быть ограничены элементами H3–H4.

headingNumbers — это начальное значение заголовка документации. Например, страница может начинаться с: {h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1}

Но если мы разбиваем документацию на несколько страниц, возможно, нам захочется начать со второй страницы: {h1: 12, h2: 1, h3: 5, h4: 8, h5: 1, h6: 10}

Что мне нужно, чтобы этот код делал в цикле над scopeHeadingElements, так это чтобы иметь возможность добавлять нумерацию документации к innerHTML.


Например, если у нас есть начальный headingNumbers: {h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1}, то следующая область HTML должна вывести следующее:

<h1>Heading 1</h1> ----> <h1>1. Heading 1</h1>
<h2>Heading 2</h2> ----> <h2>1.1. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.1.1. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>1.1.1.1. Heading 4</h4>
<h5>Heading 5</h5> ----> <h5>1.1.1.1.1. Heading 5</h5>
<h6>Heading 6</h6> ----> <h6>1.1.1.1.1.1. Heading 6</h6>

Однако если начальные headingNumbers были другими: {h1: 12, h2: 1, h3: 5, h4: 8, h5: 1, h6: 10}, то следующая область HTML должна вывести следующее:

<h1>Heading 1</h1> ----> <h1>12. Heading 1</h1>
<h2>Heading 2</h2> ----> <h2>12.1. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>12.1.5. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>12.1.5.8. Heading 4</h4>
<h5>Heading 5</h5> ----> <h5>12.1.5.8.1. Heading 5</h5>
<h6>Heading 6</h6> ----> <h6>12.1.5.8.1.10. Heading 6</h6>

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

Итак, снова пример {h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1}, тогда следующая область HTML должна вывести следующее:

<h1>Heading 1</h1> ----> <h1>1. Heading 1</h1>
<h2>Heading 2</h2> ----> <h2>1.1. Heading 2</h2>
<h2>Heading 2</h2> ----> <h2>1.2. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.2.1. Heading 3</h3>
<h3>Heading 3</h3> ----> <h3>1.2.2. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>1.2.2.1. Heading 4</h4>
<h2>Heading 2</h2> ----> <h2>1.3. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.3.1. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>1.3.1.1. Heading 4</h4>
<h5>Heading 5</h5> ----> <h5>1.3.1.1.1. Heading 5</h5>
<h6>Heading 6</h6> ----> <h6>1.3.1.1.1.1. Heading 6</h6>
<h6>Heading 6</h6> ----> <h6>1.3.1.1.1.2. Heading 6</h6>

В приведенном выше JS-коде я просто не могу понять, как заставить цикл и приращение работать без увеличения неправильного тега заголовка (некоторые попытки увеличивают значение H1, а также H2 или H1, H2 и H3). значение при увеличении H4).

Или другие попытки прибавляются слишком рано, и все начинается 1 с нуля.

Я попытался добавить - 1 к currentNumbers, однако, если levelsRange в const scopeHeadingElements = scope.querySelectorAll(levelsRange); совпадает только, скажем, с H2 по H4, тогда остальные числа становятся отрицательными или равными 0.

По сути, всем заголовкам должна быть присвоена система нумерации документации, но если они не включены в «объем», то это не влияет на HTML.

Например, если бы у нас снова был пример {h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1}, но для этого область действия была ограничена элементами от H2 до H3:

<h1>Heading 1</h1> ----> <h1>Heading 1</h1>
<h2>Heading 2</h2> ----> <h2>1.1. Heading 2</h2>
<h2>Heading 2</h2> ----> <h2>1.2. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.2.1. Heading 3</h3>
<h3>Heading 3</h3> ----> <h3>1.2.2. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>Heading 4</h4>
<h2>Heading 2</h2> ----> <h2>1.3. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.3.1. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>Heading 4</h4>
<h5>Heading 5</h5> ----> <h5>Heading 5</h5>
<h6>Heading 6</h6> ----> <h6>Heading 6</h6>
<h6>Heading 6</h6> ----> <h6>Heading 6</h6>
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
0
190
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я думаю, что цикл проще реализовать со стеком, чем с плоским объектом:

const level = 6;
const headingNumbers = {h1: 12, h2: 1, h3: 5, h4: 8, h5: 1, h6: 10};

const headers = [...document.querySelectorAll(Array.from({length: level}, (_, i) => 'h' + (i + 1)).join(','))];

let prevLevel = 0;
const stack = [];
for(const header of headers){
  const level = +header.tagName.match(/\d+/)[0];
  if (level > prevLevel) {
    const tag = header.tagName.toLowerCase();
    stack.push(headingNumbers[tag] ?? 1);
    delete headingNumbers[tag];
  } else {
    stack.splice(level, stack.length);
    stack[stack.length - 1]++;
  }
  header.insertAdjacentHTML('afterbegin', `<span>${stack.join('.')}</span>&nbsp;`);
  prevLevel = level;
}
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h6>Heading 6</h6>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h6>Heading 6</h6>

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

markb 27.05.2024 00:54

@markb, да, это установил const level = 6;

Alexander Nenashev 27.05.2024 01:01

Я это понимаю, но я обновил этот раздел следующим образом: js const levelsRange = Array.from( { length: options.levels.finish - options.levels.start + 1 }, (_, i) => 'h' + (i + options.levels.start) ).join(','); Но нумерация не имеет префикса с номерами предыдущих заголовков.

markb 27.05.2024 01:05

На самом деле для этого вам не нужен JS. Этого можно добиться только с помощью CSS, если вас устраивает поддержка браузера (которая составляет 88,75% - для counter-set - на момент написания: caniuse.com ), используя counter-reset , counter -установите , встречное приращение, ::before и content.

body {
  counter-reset: h1;
}

h1 {
  counter-reset: h2;
}

h2 {
  counter-reset: h3;
}

h3 {
  counter-reset: h4;
}

h4 {
  counter-reset: h5;
}

h5 {
  counter-reset: h6;
}

h1::before {
  counter-increment: h1;
  content: counter(h1) ". ";
}

h2::before {
  counter-increment: h2;
  content: counter(h1) "." counter(h2) ". ";
}

h3::before {
  counter-increment: h3;
  content: counter(h1) "." counter(h2) "." counter(h3) ". ";
}

h4::before {
  counter-increment: h4;
  content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
}

h5::before {
  counter-increment: h5;
  content: counter(h1) "." counter(h2) "."counter(h3) "."counter(h4) "."counter(h5) ". ";
}

h6::before {
  counter-increment: h6;
  content: counter(h1) "." counter(h2) "."counter(h3) "."counter(h4) "."counter(h5) "."counter(h6) ". ";
}
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h6>Heading 6</h6>

Чтобы начать с другого номера, используйте counter-set:

body {
  counter-set: h1 5;
}

h1 {
  counter-reset: h2;
}

h2 {
  counter-reset: h3;
}

h3 {
  counter-reset: h4;
}

h4 {
  counter-reset: h5;
}

h5 {
  counter-reset: h6;
}

h1::before {
  counter-increment: h1;
  content: counter(h1) ". ";
}

h2::before {
  counter-increment: h2;
  content: counter(h1) "." counter(h2) ". ";
}

h3::before {
  counter-increment: h3;
  content: counter(h1) "." counter(h2) "." counter(h3) ". ";
}

h4::before {
  counter-increment: h4;
  content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
}

h5::before {
  counter-increment: h5;
  content: counter(h1) "." counter(h2) "."counter(h3) "."counter(h4) "."counter(h5) ". ";
}

h6::before {
  counter-increment: h6;
  content: counter(h1) "." counter(h2) "."counter(h3) "."counter(h4) "."counter(h5) "."counter(h6) ". ";
}
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h6>Heading 6</h6>

И можно ли контролировать заголовки с нумерацией с помощью content, что можно сделать с помощью отдельного класса (как зависит от варианта использования):

body {
  counter-set: h1 5;
}

h1 {
  counter-reset: h2;
}

h2 {
  counter-reset: h3;
}

h3 {
  counter-reset: h4;
}

h4 {
  counter-reset: h5;
}

h5 {
  counter-reset: h6;
}

h1::before {
  counter-increment: h1;
  content: "";
}

h2::before {
  counter-increment: h2;
  content: counter(h1) "." counter(h2) ". ";
}

h3::before {
  counter-increment: h3;
  content: counter(h1) "." counter(h2) "." counter(h3) ". ";
}

h4::before {
  counter-increment: h4;
  content: "";
}

h5::before {
  counter-increment: h5;
  content: "";
}

h6::before {
  counter-increment: h6;
  content: "";
}
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h6>Heading 6</h6>

Если вы не хотите включать файл CSS, вы также можете создать CSS программно в JS. Вот еще не полностью протестированный пример:

let sheet = new CSSStyleSheet();

let startNumbers = [3];
let applyNumbering = [false, true, true, true];
let maxHNumber = 6;


function createCounterReset(from, to) {
  let result = [];
  for (let i = from; i <= to; i++) {
    result.push(`h${i}`);
  }
  if (result.length == 0) {
    return ""
  }
  return `counter-reset: ${result.join(' ')};`;
}

function createCounter(to) {
  let result = [];
  for (let i = 0; i < to; i++) {
    result.push(`counter(h${i+1})`);
  }
  return result.join(' "." ') + ' ". "';
}

let style = [];
style.push('body {');
if (startNumbers.length > 0) {
  style.push('counter-set: ' + startNumbers.map((value, idx) => `h${idx+1} ${value-1}`).join(' ')) + ';'
}
style.push('}');


for (let i = 0; i < maxHNumber; i++) {
  style.push(`h${i+1} {
    ${createCounterReset(i+2, maxHNumber)}
    counter-increment: h${i+1};
    }`);

  if (applyNumbering[i]) {
    style.push(`h${i+1}::before {
      content: ${createCounter(i+1)};
    }`);
  }
}

sheet.replaceSync(style.join("\n"));
document.adoptedStyleSheets = [sheet];
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h6>Heading 6</h6>

Привет @t.niese! Спасибо за этот код и за работу над обеими версиями (все заголовки и заголовки с ограниченной областью действия). Но этот плагин JS будет частью более крупного фреймворка, работающего только с JS, поэтому планируется использовать только JS.

markb 29.05.2024 00:20

@markb Почему в этом случае использование CSS является проблемой? Вы хотите включить только файл .js? Если да, то можно ли программно создать CSS? Я имею в виду, да, это наверняка можно было бы сделать с помощью JS, но использование CSS гораздо удобнее в обслуживании (например, в случае динамического изменения HTML).

t.niese 29.05.2024 00:26

@markb Добавлен пример того, как можно программно создать CSS в JS.

t.niese 29.05.2024 01:18
Ответ принят как подходящий

Учитывается ли подход «только CSS»?

Следующее предоставленное решение создает/обеспечивает правильную нумерацию с помощью css-сгенерированного контента , используя ::before псевдоэлемент и сопровождающее его свойство content , где значения счетчика записываются через функция counter() CSS. А что касается правильного подсчета, существуют специальные правила для заголовков и счетчиков, которые либо создают/сбрасывают , либо увеличивают целевые счетчики.

h1, h2, h3, h4, h5, h6 {
  margin: 4px 0;
}
body {
  margin: 0;

  counter-reset: h1 h2 h3 h4 h5 h6;
}

h1 {
  font-size: 1.6em;

  counter-reset: h2 h3 h4 h5 h6;
  counter-increment: h1;
}
h2 {
  font-size: 1.4em;

  counter-reset: h3 h4 h5 h6;
  counter-increment: h2;
}
h3 {
  font-size: 1.25em;

  counter-reset: h4 h5 h6;
  counter-increment: h3;
}
h4 {
  font-size: 1.1em;

  counter-reset: h5 h6;
  counter-increment: h4;
}
h5 {
  font-size: 1em;

  counter-reset: h6;
  counter-increment: h5;
}
h6 {
  font-size: .9em;

  counter-increment: h6;
}

h1::before { content: counter(h1) ". "; }
h2::before { content: counter(h1) "." counter(h2) ". "; }
h3::before { content: counter(h1) "." counter(h2) "." counter(h3) ". "; }
h4::before { content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". "; }
h5::before { content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". "; }
h6::before { content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". "; }
<h1>Heading-1</h1>

  <h2>Heading-2</h2>

    <h3>Heading-3</h3>
    <h3>Heading-3</h3>
    
      <h4>Heading-4</h4>
      <h4>Heading-4</h4>

  <h2>Heading-2</h2>

    <h3>Heading-3</h3>
    <h3>Heading-3</h3>

      <h4>Heading-4</h4>

  <h2>Heading-2</h2>

    <h3>Heading-3</h3>

      <h4>Heading-4</h4>

        <h5>Heading-5</h5>

          <h6>Heading-6</h6>
          <h6>Heading-6</h6>


<h1>Heading-1</h1>

  <h2>Heading-2</h2>
  <h2>Heading-2</h2>

    <h3>Heading-3</h3>
    <h3>Heading-3</h3>

      <h4>Heading-4</h4>

  <h2>Heading-2</h2>

    <h3>Heading-3</h3>

      <h4>Heading-4</h4>

        <h5>Heading-5</h5>

          <h6>Heading-6</h6>
          <h6>Heading-6</h6>


<h1>Heading-1</h1>

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

h1, h2, h3, h4, h5, h6 {
  margin: 4px 0;
}
body {
  margin: 0;

  /* counter-reset: h1 h2 h3 h4 h5 h6; */
}
section {
  margin: 0 0 16px 0;
  border: 1px dashed black;

  counter-reset: h1 h2 h3 h4 h5 h6;
}

h1 {
  font-size: 1.6em;

  counter-reset: h2 h3 h4 h5 h6;
  counter-increment: h1;
}
h2 {
  font-size: 1.4em;

  counter-reset: h3 h4 h5 h6;
  counter-increment: h2;
}
h3 {
  font-size: 1.25em;

  counter-reset: h4 h5 h6;
  counter-increment: h3;
}
h4 {
  font-size: 1.1em;

  counter-reset: h5 h6;
  counter-increment: h4;
}
h5 {
  font-size: 1em;

  counter-reset: h6;
  counter-increment: h5;
}
h6 {
  font-size: .9em;

  counter-increment: h6;
}

h1::before { content: counter(h1) ". "; }
h2::before { content: counter(h1) "." counter(h2) ". "; }
h3::before { content: counter(h1) "." counter(h2) "." counter(h3) ". "; }
h4::before { content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". "; }
h5::before { content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". "; }
h6::before { content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". "; }

.heading-count-not-for {
  &.h1 { h1::before { content: ""; } }
  &.h2 { h2::before { content: ""; } }
  &.h3 { h3::before { content: ""; } }
  &.h4 { h4::before { content: ""; } }
  &.h5 { h5::before { content: ""; } }
  &.h6 { h6::before { content: ""; } }
}
<section class = "heading-count-not-for h4 h5 h6">
  <h1>Heading-1</h1>

    <h2>Heading-2</h2>

      <h3>Heading-3</h3>
      <h3>Heading-3</h3>

        <h4>Heading-4</h4>
        <h4>Heading-4</h4>

    <h2>Heading-2</h2>

      <h3>Heading-3</h3>
      <h3>Heading-3</h3>

        <h4>Heading-4</h4>

    <h2>Heading-2</h2>

      <h3>Heading-3</h3>

        <h4>Heading-4</h4>

          <h5>Heading-5</h5>

            <h6>Heading-6</h6>
            <h6>Heading-6</h6>


  <h1>Heading-1</h1>

    <h2>Heading-2</h2>
    <h2>Heading-2</h2>

      <h3>Heading-3</h3>
      <h3>Heading-3</h3>

        <h4>Heading-4</h4>

    <h2>Heading-2</h2>

      <h3>Heading-3</h3>

        <h4>Heading-4</h4>

          <h5>Heading-5</h5>

            <h6>Heading-6</h6>
            <h6>Heading-6</h6>


  <h1>Heading-1</h1>
</section>

<section class = "heading-count-not-for h1 h3 h4 h5 h6">
  <h1>Heading-1</h1>

    <h2>Heading-2</h2>

      <h3>Heading-3</h3>
      <h3>Heading-3</h3>

        <h4>Heading-4</h4>
        <h4>Heading-4</h4>

    <h2>Heading-2</h2>

      <h3>Heading-3</h3>
      <h3>Heading-3</h3>

        <h4>Heading-4</h4>

    <h2>Heading-2</h2>

      <h3>Heading-3</h3>

        <h4>Heading-4</h4>

          <h5>Heading-5</h5>

            <h6>Heading-6</h6>
            <h6>Heading-6</h6>
</section>


Отредактируйте, чтобы предоставить решение на основе JS, как первоначально запрошено ОП.

... цитируя комментарий ОП из моего ответа / ответов "только для CSS" выше...

Привет @Питер Селигер! Спасибо за этот код, и хотя он прекрасно работает как решение CSS, этот JS-код станет частью более крупного первого продукта JS, поэтому во время компиляции он будет статически встроен в DOM. Если у вас есть версия только для JS, я бы с удовольствием ее увидел! - отметка

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

Сама реализация имитирует поведение ранее предоставленных примеров кода «только для CSS» с их конкретными правилами для счетчиков заголовков, особенно в отношении использования counter-reset и counter-increment.

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

Последнее делается путем присвоения вычисленного значения свойству dataset .counter такого элемента, которое немедленно изменит пользовательское data- counter атрибут-значение самого элемента, при этом последнее будет помещено ::before в соответствующий заголовок -element по одному общему правилу CSS.

1-й пример кода

Использование ОП...

  • настраиваемые разные/различные области заголовков
  • но ожидаемые базовые значения счетчика по умолчанию равны 1...

// begin ... counting-implementation or counting-module.

function createCountContextObjects(config) {
  const headingDefault = {
    counter: 1,
    inScope: true,
  };
  const { h1, h2, h3, h4, h5, h6 } = (config || {});

  const configEntries = Object
    .entries({
      h1: { ...headingDefault, ...(h1 || {}) },
      h2: { ...headingDefault, ...(h2 || {}) },
      h3: { ...headingDefault, ...(h3 || {}) },
      h4: { ...headingDefault, ...(h4 || {}) },
      h5: { ...headingDefault, ...(h5 || {}) },
      h6: { ...headingDefault, ...(h6 || {}) },
    });

  const currentCounts = new Map(
    configEntries
      .map(([key, { counter }]) => {

        counter = parseInt(counter, 10);
        counter = Number.isFinite(counter) ? (counter - 1) : 0;

        return [key, { reset: counter, current: counter }]
      })
  );
  const scopedTagNames = new Set(
    configEntries 
      .filter(([_, { inScope }]) => inScope)
      .map(([key]) => key)
  );

  return {
    currentCounts,
    scopedTagNames,
  };
}

function applyCurrentCountThroughBoundContext(headingNode) {
  const { currentCounts, scopedTagNames } = this;

  const headingName = headingNode.tagName.toLowerCase();
  const headingNameList = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];

  const nextMinorName = headingNameList
    .at(headingNameList.indexOf(headingName) + 1);

  if (nextMinorName) {
    const nextMinorCounts = currentCounts.get(nextMinorName);

    // - set the next minor heading-specific count to its
    //   default configuered `reset` value exactly once;
    // - after having applied the `reset`-default once, reset
    //   the next minor heading-specific count always to zero.
    nextMinorCounts.current = nextMinorCounts.reset;
    nextMinorCounts.reset = 0;
  }
  const counts = currentCounts.get(headingName);

  // - increment the heading-specific count
  //   of the currently processed heading-node.
  counts.current = counts.current + 1;

  if (scopedTagNames.has(headingName)) {

    // - compute and assign the correct counter-value
    //   according to both, the custom heading configuration
    //   and the currently processed node's heading-prescedence.

    headingNode.dataset.counter =
      headingNameList
        .slice(
          // - get all tag names of higher and equal
          //   heading-prescedence compared to the
          //   currently processed heading-node.
          0, headingNameList.indexOf(headingName) + 1
        )
        .reduce((counterList, name) =>
          counterList.concat(currentCounts.get(name).current), []
        )
        .join('.') + ".";
  }
}

/*export */function applyScopedHeadingCounts(config) {
  const {
    currentCounts,
    scopedTagNames,
  } =
    createCountContextObjects(config);

  const headingList = [
    ...document
      .querySelectorAll('h1, h2, h3, h4, h5, h6')
  ];

  console.info({
    customConfig: config,
    scopedTagNames: [...scopedTagNames.values()],
    currentCounts: Object.fromEntries(currentCounts.entries()),
    headingList,
  });

  headingList
    .forEach(
      applyCurrentCountThroughBoundContext, {
        currentCounts, scopedTagNames,
      },
    );
}
// end ... counting-implementation or counting-module.


// another script-block's or module's scope
applyScopedHeadingCounts(customConfig);
body { margin: 0; }

h1, h2, h3, h4, h5, h6 { margin: 4px 0; }

h1 { font-size: 1.6em; }
h2 { font-size: 1.4em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1.1em; }
h5 { font-size: 1em; }
h6 { font-size: .9em; }

h1, h2, h3, h4, h5, h6 {
  &::before { content: attr(data-counter) " "; }
}
.as-console-wrapper { left: auto!important; width: 50%; min-height: 100%; }
<h1>Heading 1</h1> <!-- <h1>Heading 1</h1>        -->
<h2>Heading 2</h2> <!-- <h2>1.1. Heading 2</h2>   -->
<h2>Heading 2</h2> <!-- <h2>1.2. Heading 2</h2>   -->
<h3>Heading 3</h3> <!-- <h3>1.2.1. Heading 3</h3> -->
<h3>Heading 3</h3> <!-- <h3>1.2.2. Heading 3</h3> -->
<h4>Heading 4</h4> <!-- <h4>Heading 4</h4>        -->
<h2>Heading 2</h2> <!-- <h2>1.3. Heading 2</h2>   -->
<h3>Heading 3</h3> <!-- <h3>1.3.1. Heading 3</h3> -->
<h4>Heading 4</h4> <!-- <h4>Heading 4</h4>        -->
<h5>Heading 5</h5> <!-- <h5>Heading 5</h5>        -->
<h6>Heading 6</h6> <!-- <h6>Heading 6</h6>        -->
<h6>Heading 6</h6> <!-- <h6>Heading 6</h6>        -->

<script>
  // config-file or inline-config

  const customConfig = {
    h1: { inScope: false },
    h4: { inScope: false },
    h5: { inScope: false },
    h6: { inScope: false },
  }
  /* ... same as ... */

  // const customConfig = {
  //   h1: { counter: 1, inScope: false },
  //   h2: { counter: 1, inScope: true },
  //   h3: { counter: 1, inScope: true },
  //   h4: { counter: 1, inScope: false },
  //   h5: { counter: 1, inScope: false },
  //   h6: { counter: 1, inScope: false },
  // }
</script>

<!--
<script>
  applyScopedHeadingCounts(customConfig);
</script>
//-->

второй пример кода

Использование ОП...

  • ожидаемые области заголовков по умолчанию (видны все счетчики)
  • и некоторые пользовательские базовые значения счетчиков, отличные от 1...

// begin ... counting-implementation or counting-module.

function createCountContextObjects(config) {
  const headingDefault = {
    counter: 1,
    inScope: true,
  };
  const { h1, h2, h3, h4, h5, h6 } = (config || {});

  const configEntries = Object
    .entries({
      h1: { ...headingDefault, ...(h1 || {}) },
      h2: { ...headingDefault, ...(h2 || {}) },
      h3: { ...headingDefault, ...(h3 || {}) },
      h4: { ...headingDefault, ...(h4 || {}) },
      h5: { ...headingDefault, ...(h5 || {}) },
      h6: { ...headingDefault, ...(h6 || {}) },
    });

  const currentCounts = new Map(
    configEntries
      .map(([key, { counter }]) => {

        counter = parseInt(counter, 10);
        counter = Number.isFinite(counter) ? (counter - 1) : 0;

        return [key, { reset: counter, current: counter }]
      })
  );
  const scopedTagNames = new Set(
    configEntries 
      .filter(([_, { inScope }]) => inScope)
      .map(([key]) => key)
  );

  return {
    currentCounts,
    scopedTagNames,
  };
}

function applyCurrentCountThroughBoundContext(headingNode) {
  const { currentCounts, scopedTagNames } = this;

  const headingName = headingNode.tagName.toLowerCase();
  const headingNameList = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];

  const nextMinorName = headingNameList
    .at(headingNameList.indexOf(headingName) + 1);

  if (nextMinorName) {
    const nextMinorCounts = currentCounts.get(nextMinorName);

    // - set the next minor heading-specific count to its
    //   default configuered `reset` value exactly once;
    // - after having applied the `reset`-default once, reset
    //   the next minor heading-specific count always to zero.
    nextMinorCounts.current = nextMinorCounts.reset;
    nextMinorCounts.reset = 0;
  }
  const counts = currentCounts.get(headingName);

  // - increment the heading-specific count
  //   of the currently processed heading-node.
  counts.current = counts.current + 1;

  if (scopedTagNames.has(headingName)) {

    // - compute and assign the correct counter-value
    //   according to both, the custom heading configuration
    //   and the currently processed node's heading-prescedence.

    headingNode.dataset.counter =
      headingNameList
        .slice(
          // - get all tag names of higher and equal
          //   heading-prescedence compared to the
          //   currently processed heading-node.
          0, headingNameList.indexOf(headingName) + 1
        )
        .reduce((counterList, name) =>
          counterList.concat(currentCounts.get(name).current), []
        )
        .join('.') + ".";
  }
}

/*export */function applyScopedHeadingCounts(config) {
  const {
    currentCounts,
    scopedTagNames,
  } =
    createCountContextObjects(config);

  const headingList = [
    ...document
      .querySelectorAll('h1, h2, h3, h4, h5, h6')
  ];

  console.info({
    customConfig: config,
    scopedTagNames: [...scopedTagNames.values()],
    currentCounts: Object.fromEntries(currentCounts.entries()),
    headingList,
  });

  headingList
    .forEach(
      applyCurrentCountThroughBoundContext, {
        currentCounts, scopedTagNames,
      },
    );
}
// end ... counting-implementation or counting-module.


// another script-block's or module's scope
applyScopedHeadingCounts(customConfig);
body { margin: 0; }

h1, h2, h3, h4, h5, h6 { margin: 4px 0; }

h1 { font-size: 1.6em; }
h2 { font-size: 1.4em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1.1em; }
h5 { font-size: 1em; }
h6 { font-size: .9em; }

h1, h2, h3, h4, h5, h6 {
  &::before { content: attr(data-counter) " "; }
}
.as-console-wrapper { left: auto!important; width: 50%; min-height: 100%; }
<h1>Heading 1</h1> <!-- <h1>12. Heading 1</h1>            -->
<h2>Heading 2</h2> <!-- <h2>12.1. Heading 2</h2>          -->
<h3>Heading 3</h3> <!-- <h3>12.1.5. Heading 3</h3>        -->
<h4>Heading 4</h4> <!-- <h4>12.1.5.8. Heading 4</h4>      -->
<h5>Heading 5</h5> <!-- <h5>12.1.5.8.1. Heading 5</h5>    -->
<h6>Heading 6</h6> <!-- <h6>12.1.5.8.1.10. Heading 6</h6> -->
<h6>Heading 6</h6> <!-- <h6>12.1.5.8.1.11. Heading 6</h6> -->
<h3>Heading 3</h3> <!-- <h3>12.1.6. Heading 3</h3>        -->
<h4>Heading 4</h4> <!-- <h4>12.1.6.1. Heading 4</h4>      -->
<h5>Heading 5</h5> <!-- <h5>12.1.6.1.1. Heading 5</h5>    -->
<h6>Heading 6</h6> <!-- <h6>12.1.6.1.1.1. Heading 6</h6>  -->
<h6>Heading 6</h6> <!-- <h6>12.1.6.1.1.2. Heading 6</h6>  -->

<script>
  // config-file or inline-config

  const customConfig = {
    h1: { counter: 12 },
    h3: { counter: 5 },
    h4: { counter: 8 },
    h6: { counter: 10 },
  }
  /* ... same as ... */

  // const customConfig = {
  //   h1: { counter: 12, inScope: true },
  //   h2: { counter: 1, inScope: true },
  //   h3: { counter: 5, inScope: true },
  //   h4: { counter: 8, inScope: true },
  //   h5: { counter: 1, inScope: true },
  //   h6: { counter: 10, inScope: true },
  // }
</script>

<!--
<script>
  applyScopedHeadingCounts(customConfig);
</script>
//-->

Третий пример кода

Одна из вышеприведенных разметок заголовков «только для CSS» с...

  • ожидаемые области заголовков по умолчанию (видны все счетчики)
  • и некоторые пользовательские базовые значения счетчиков, отличные от 1...

// begin ... counting-implementation or counting-module.

function createCountContextObjects(config) {
  const headingDefault = {
    counter: 1,
    inScope: true,
  };
  const { h1, h2, h3, h4, h5, h6 } = (config || {});

  const configEntries = Object
    .entries({
      h1: { ...headingDefault, ...(h1 || {}) },
      h2: { ...headingDefault, ...(h2 || {}) },
      h3: { ...headingDefault, ...(h3 || {}) },
      h4: { ...headingDefault, ...(h4 || {}) },
      h5: { ...headingDefault, ...(h5 || {}) },
      h6: { ...headingDefault, ...(h6 || {}) },
    });

  const currentCounts = new Map(
    configEntries
      .map(([key, { counter }]) => {

        counter = parseInt(counter, 10);
        counter = Number.isFinite(counter) ? (counter - 1) : 0;

        return [key, { reset: counter, current: counter }]
      })
  );
  const scopedTagNames = new Set(
    configEntries 
      .filter(([_, { inScope }]) => inScope)
      .map(([key]) => key)
  );

  return {
    currentCounts,
    scopedTagNames,
  };
}

function applyCurrentCountThroughBoundContext(headingNode) {
  const { currentCounts, scopedTagNames } = this;

  const headingName = headingNode.tagName.toLowerCase();
  const headingNameList = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];

  const nextMinorName = headingNameList
    .at(headingNameList.indexOf(headingName) + 1);

  if (nextMinorName) {
    const nextMinorCounts = currentCounts.get(nextMinorName);

    // - set the next minor heading-specific count to its
    //   default configuered `reset` value exactly once;
    // - after having applied the `reset`-default once, reset
    //   the next minor heading-specific count always to zero.
    nextMinorCounts.current = nextMinorCounts.reset;
    nextMinorCounts.reset = 0;
  }
  const counts = currentCounts.get(headingName);

  // - increment the heading-specific count
  //   of the currently processed heading-node.
  counts.current = counts.current + 1;

  if (scopedTagNames.has(headingName)) {

    // - compute and assign the correct counter-value
    //   according to both, the custom heading configuration
    //   and the currently processed node's heading-prescedence.

    headingNode.dataset.counter =
      headingNameList
        .slice(
          // - get all tag names of higher and equal
          //   heading-prescedence compared to the
          //   currently processed heading-node.
          0, headingNameList.indexOf(headingName) + 1
        )
        .reduce((counterList, name) =>
          counterList.concat(currentCounts.get(name).current), []
        )
        .join('.') + ".";
  }
}

/*export */function applyScopedHeadingCounts(config) {
  const {
    currentCounts,
    scopedTagNames,
  } =
    createCountContextObjects(config);

  const headingList = [
    ...document
      .querySelectorAll('h1, h2, h3, h4, h5, h6')
  ];

  console.info({
    customConfig: config,
    scopedTagNames: [...scopedTagNames.values()],
    currentCounts: Object.fromEntries(currentCounts.entries()),
    headingList,
  });

  headingList
    .forEach(
      applyCurrentCountThroughBoundContext, {
        currentCounts, scopedTagNames,
      },
    );
}
// end ... counting-implementation or counting-module.


// another script-block's or module's scope
applyScopedHeadingCounts(customConfig);
body { margin: 0; }

h1, h2, h3, h4, h5, h6 { margin: 4px 0; }

h1 { font-size: 1.6em; }
h2 { font-size: 1.4em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1.1em; }
h5 { font-size: 1em; }
h6 { font-size: .9em; }

h1, h2, h3, h4, h5, h6 {
  &::before { content: attr(data-counter) " "; }
}
.as-console-wrapper { left: auto!important; width: 50%; min-height: 100%; }
<h1>Heading-1</h1>

  <h2>Heading-2</h2>

    <h3>Heading-3</h3>
    <h3>Heading-3</h3>
    
      <h4>Heading-4</h4>
      <h4>Heading-4</h4>

  <h2>Heading-2</h2>

    <h3>Heading-3</h3>
    <h3>Heading-3</h3>

      <h4>Heading-4</h4>

  <h2>Heading-2</h2>

    <h3>Heading-3</h3>

      <h4>Heading-4</h4>

        <h5>Heading-5</h5>

          <h6>Heading-6</h6>
          <h6>Heading-6</h6>


<h1>Heading-1</h1>

  <h2>Heading-2</h2>
  <h2>Heading-2</h2>

    <h3>Heading-3</h3>
    <h3>Heading-3</h3>

      <h4>Heading-4</h4>

  <h2>Heading-2</h2>

    <h3>Heading-3</h3>

      <h4>Heading-4</h4>

        <h5>Heading-5</h5>

          <h6>Heading-6</h6>
          <h6>Heading-6</h6>


<h1>Heading-1</h1>


<script>
  // config-file or inline-config

  const customConfig = {
    h1: { counter: 10 },
    h2: { counter: 4 },
    h3: { counter: 7 },
  }
</script>

<!--
<script>
  applyScopedHeadingCounts(customConfig);
</script>
//-->

Привет @Питер Селигер! Спасибо за этот код, и хотя он прекрасно работает как решение CSS, этот JS-код станет частью более крупного первого продукта JS, поэтому во время компиляции он будет статически встроен в DOM. Если у вас есть версия только для JS, я бы с удовольствием ее увидел!

markb 29.05.2024 00:19

@markb ... Я обновил свой ответ в соответствии с вашим первоначальным запросом. Новое редактирование посвящено решению, основанному на JS.

Peter Seliger 31.05.2024 11:43

! Ух ты, это всеобъемлющее и ценное описание различных сценариев. Я определенно могу работать с этим и прекрасно интегрировать его в базу кода. И JS, и CSS-решения работают отлично, так как я уверен, что найдутся и другие, нуждающиеся в обоих вариантах.

markb 31.05.2024 13:56

Другие вопросы по теме