Я создал плагин 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>



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Я думаю, что цикл проще реализовать со стеком, чем с плоским объектом:
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> `);
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, да, это установил const level = 6;
Я это понимаю, но я обновил этот раздел следующим образом: js const levelsRange = Array.from( { length: options.levels.finish - options.levels.start + 1 }, (_, i) => 'h' + (i + options.levels.start) ).join(','); Но нумерация не имеет префикса с номерами предыдущих заголовков.
На самом деле для этого вам не нужен 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 Почему в этом случае использование CSS является проблемой? Вы хотите включить только файл .js? Если да, то можно ли программно создать CSS? Я имею в виду, да, это наверняка можно было бы сделать с помощью JS, но использование CSS гораздо удобнее в обслуживании (например, в случае динамического изменения HTML).
@markb Добавлен пример того, как можно программно создать CSS в JS.
Учитывается ли подход «только 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-й пример кода
Использование ОП...
// 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>
//-->второй пример кода
Использование ОП...
// 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» с...
// 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 ... Я обновил свой ответ в соответствии с вашим первоначальным запросом. Новое редактирование посвящено решению, основанному на JS.
! Ух ты, это всеобъемлющее и ценное описание различных сценариев. Я определенно могу работать с этим и прекрасно интегрировать его в базу кода. И JS, и CSS-решения работают отлично, так как я уверен, что найдутся и другие, нуждающиеся в обоих вариантах.
Очень интересно и умно! Во-первых, это последний блок кода, который у меня есть в исходном вопросе, где есть область видимости тегов заголовков, к которым можно добавлять числа. Будет ли ваша версия адаптирована для реализации и этого?