見出しタグから目次を作るJavascript(Typescript)
2022年1月5日
2022年1月10日
目次
ブログ記事を書いていると、「目次を入れたいなー」と思うことがよくあります。
自分で毎回作ってもいいのですが、見出しをとってきて自動的に作って欲しいなと思い作ってみました。
ここのブログでもこれを使って自動生成しています。
目次用のTypescript
export interface TableOfContentsSection {
el?: HTMLHeadingElement
items?: TableOfContentsSection[]
}
export interface TableOfContentsItem {
title?: string
id?: string
items?: TableOfContentsItem[]
}
export const createChildrenItem = (
elements: HTMLHeadingElement[],
currentLabel: number,
tableOfContentsItems?: TableOfContentsItem[],
isBreak?: boolean
) => {
const items: TableOfContentsItem[] = tableOfContentsItems || []
for(let i = 0; i < elements.length; i++) {
const el = elements[i]
const label = parseInt(el?.tagName.replace('H', ''))
const nextElement = elements[i + 1]
const nextLabel = parseInt(nextElement?.tagName.replace('H', ''))
const item: TableOfContentsSection = { el }
if(nextLabel > currentLabel) {
item.items = createChildrenItem(elements.slice(i + 1), nextLabel, item.items , true)
}
if(label > currentLabel) {
continue
}
if(isBreak && label < currentLabel) {
break
}
items.push(item)
}
return items
}
export const createTableOfContents = (hTagElements?: NodeListOf<Element>): TableOfContentsItem[] | undefined => {
if(!hTagElements) return undefined
return createSection(
createChildrenItem(Array.prototype.slice.call(hTagElements), 2)
)
}
const createSection = (section: TableOfContentsSection[], str?: string): TableOfContentsItem[] => {
return section.map((h, i) => {
const title = generateDescription(h.el?.innerHTML)
const numString = `${str ? `${str}-` : ''}${i}`
const item: TableOfContentsItem = {
title,
id: getId(h.el!, numString, title)
}
if (h.items) {
item.items = createSection(h.items, numString)
}
return item
})
}
export const generateDescription = (str?: string): string => {
return str?.split('\n').map(value =>
value.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, '')
).join('') || ''
}
const getId = (el: HTMLHeadingElement, str?: string, title?: string) => {
const childFirstContent: Element | null = el.querySelector('*')
const id = el.id || childFirstContent?.id || `${str ? `${str}_` : ''}${title}`
el.setAttribute('id', id )
return id
}
実際に上のJSを使ってHTMLを吐き出す
上のTypescript
はhタグ
をもとに階層構造のjson
を吐き出す実装になっています。
そのjson
を使って、実際にタグを生成する部分の実装です。
通常の静的なHTMLにタグを生成する場合、Typescript
import { TableOfContentsItem } from '~/utils/createTableOfContents'
const createTableContentsElement = (items: TableOfContentsItem) => {
const createTableOfContent = (value: TableOfContentsItem) => {
const ul = document.createElement('ul')
ul.classList.add('table-of-contents')
value.items?.map((item: TableOfContentsItem) => {
const li = document.createElement('li');
li.classList.add('table-of-contents-value')
const a = document.createElement('a');
a.textContent = item.title;
li.appendChild(a);
ul.appendChild(li);
if(item.items) {
li.appendChild(createTableOfContent(item))
}
})
return ul
}
return createTableOfContent(items)
}
const tableContents = createTableContentsElement({ items: createTableOfContents(document.querySelectorAll('h2, h3, h4, h5, h6'))})
const h1 = document.querySelector('h1')
document.querySelector('body').insertBefore(tableContents, h1.nextSibling)
実際に吐き出されるHTML
下記のように階層構造のul
が吐き出されます。
<ul class="table-of-contents">
<li class="table-of-contents-value"><a>1. この文章は見出しですよ</a>
<ul class="table-of-contents">
<li class="table-of-contents-value"><a>1.1 この文章は見出しですよ</a></li>
</ul>
</li>
<li class="table-of-contents-value"><a>2. この文章は見出しですよ</a>
<ul class="table-of-contents">
<li class="table-of-contents-value"><a>2.1 この文章は見出しですよ</a></li>
<li class="table-of-contents-value"><a>2.2 この文章は見出しですよ</a>
<ul class="table-of-contents">
<li class="table-of-contents-value"><a>2.2.1 この文章は見出しですよ</a></li>
<li class="table-of-contents-value"><a>2.2.2 この文章は見出しですよ</a>
<ul class="table-of-contents">
<li class="table-of-contents-value"><a>2.2.2.1 この文章は見出しですよ</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
Reactの目次コンポーネント(Typescript)
import React from 'react'
import { TableOfContentsItem } from '~/utils/createTableOfContents'
export interface TableOfContentsProps {
items?: TableOfContentsItem[]
}
const TableOfContents: React.FC<TableOfContentsProps> = props => {
const createTableOfContents = (value: TableOfContentsItem, num: number) => {
return (
<ul className={`lebel${num}`}>
{
value.items?.map((item) => (
<li key={item.id}>
<a href={`#${item.id}`}>{ item.title }</a>
{item.items && (
createTableOfContents(item, num + 1)
)}
</li>
))
}
</ul>
)
}
return <section>
<h2>目次</h2>
{createTableOfContents({ items: props.items! }, 2)}
</section>
}
export default TableOfContents
まとめ
ちょっと細かい説明は今回、少し省いて書いちゃいました。
ないかなーと少し探したときに、あまり目次を自動生成するJavascript
を見つけることができなかったので
参考になれば良いかなーと言う感じで残しておきます!
npmでプラグインかしちゃっても良いかなーと思ったのですが、もうちょっと整理したりしたいところもあったので
整理したらまた改めて記事にするかもしれないです。