Last active
January 26, 2021 15:39
-
-
Save sunghwan2789/a5acb5431bfa36681788ba9f2b166019 to your computer and use it in GitHub Desktop.
카테고리에 속하는 글을 위키 문서처럼 바꾸어 보아요 - 제목 태그 순번 매기기, 목차 만들기 를 자동으로 해드려요
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.wiki-heading__collapsible { | |
cursor: pointer; | |
/* border-bottom: 1px solid #000; */ | |
} | |
.wiki-heading__collapsible::before { | |
content: '^'; | |
display: inline-block; | |
transform: translateY(-5%) rotate(180deg); | |
font-weight: 100; | |
filter: opacity(0.5); | |
} | |
.wiki-heading__collapsible.collapsed::before { | |
transform: translateX(-5%) rotate(90deg); | |
} | |
.wiki-heading__toc { | |
border: 1px solid #999; | |
display: inline-block; | |
padding: .5em; | |
} | |
.wiki-heading__toc-header { | |
font-size: 1.2em; | |
} | |
.wiki-heading__toc-item.h1 { | |
text-indent: 0em; | |
} | |
.wiki-heading__toc-item.h2 { | |
text-indent: 1em; | |
} | |
.wiki-heading__toc-item.h3 { | |
text-indent: 2em; | |
} | |
.wiki-heading__toc-item.h4 { | |
text-indent: 3em; | |
} | |
.wiki-heading__toc-item.h5 { | |
text-indent: 4em; | |
} | |
.wiki-heading__toc-item.h6 { | |
text-indent: 5em; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link rel="stylesheet" href="wiki-headings.css"> | |
<div class="article"> | |
<div class="article-category">마르네</div> | |
<h2>0</h2> | |
<h1>1</h1> | |
<p>adsf<br> | |
eoewo</p> | |
<p>asdfar</p> | |
<h2>2</h2> | |
<h3>3</h3> | |
<h2>22</h2> | |
<h2>222</h2> | |
<h3>33</h3> | |
<h5>5</h5> | |
<h1>11</h1> | |
<h2>22</h2> | |
<div class="container_postbtn"></div> | |
<h3>agsdf</h3> | |
</div> | |
<script src="wiki-headings.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function applyWikiHeadings() { | |
const targetCategories = new Set([ | |
'마르네', | |
]); | |
const collapsibleHeadingClassName = 'wiki-heading__collapsible'; | |
const collapsibleHeadingCollapsedClassName = 'collapsed'; | |
const tocClassName = 'wiki-heading__toc'; | |
const tocHeaderClassName = 'wiki-heading__toc-header'; | |
const tocItemClassName = 'wiki-heading__toc-item'; | |
document.addEventListener('DOMContentLoaded', _ => { | |
getTargetArticles().forEach(applyWikiHeading); | |
}, false); | |
function getTargetArticles() { | |
const articles = Array.from(document.querySelectorAll('.article')); | |
return articles.filter(article => { | |
const { textContent: articleCategory } = article.querySelector('.article-category'); | |
return targetCategories.has(articleCategory.split('/')[0]); | |
}); | |
} | |
/** | |
* @param {Element} article | |
*/ | |
function applyWikiHeading(article) { | |
const articleEndNode = article.querySelector('.container_postbtn'); | |
const headings = Array.from(article.querySelectorAll('h1,h2,h3,h4,h5,h6')) | |
.filter(header => header.compareDocumentPosition(articleEndNode) & 4); | |
bindCollapsible(article, headings); | |
const headingTree = Array.from(buildHeadingTree(headings)); | |
insertTableOfContent(article, headingTree); | |
injectHeadingNumber(article, headingTree); | |
} | |
/** | |
* @param {Element} article | |
* @param {Element[]} headings | |
*/ | |
function bindCollapsible(article, headings) { | |
const articleEndNode = article.querySelector('.container_postbtn'); | |
const breakpoints = [...headings.slice(1), articleEndNode]; | |
const collapsibles = headings.map((heading, index) => [heading, breakpoints[index]]); | |
collapsibles.forEach(([heading, breakpoint]) => { | |
heading.classList.add(collapsibleHeadingClassName); | |
heading.addEventListener('click', e => { | |
if (e.target !== heading) { | |
return; | |
} | |
heading.classList.toggle(collapsibleHeadingCollapsedClassName); | |
let element = heading.nextElementSibling; | |
while (element !== breakpoint) { | |
element.toggleAttribute('hidden'); | |
element = element.nextElementSibling; | |
} | |
}); | |
}) | |
} | |
/** | |
* @param {Element} article | |
* @param {*} headingTree | |
*/ | |
function insertTableOfContent(article, headingTree) { | |
const tocItems = Array.from(buildTableOfContent(headingTree)); | |
const { heading: firstHeading } = headingTree[0]; | |
firstHeading.insertAdjacentHTML('beforebegin', ` | |
<div class="${tocClassName}"> | |
<div class="${tocHeaderClassName}"> | |
${'목차'} | |
</div> | |
${tocItems.join('')} | |
</div> | |
`); | |
function* buildTableOfContent(tree, parentHeadingNumber) { | |
let internalNumber = 1; | |
for (const { heading, children } of tree) { | |
const headingNumber = `${parentHeadingNumber || ''}${internalNumber}.`; | |
yield createTocItem(heading, headingNumber); | |
yield* buildTableOfContent(children, headingNumber); | |
internalNumber += 1; | |
} | |
} | |
/** | |
* @param {Element} heading | |
* @param {string} headingNumber | |
*/ | |
function createTocItem(heading, headingNumber) { | |
const headingLevel = headingNumber.split('.').length - 1; | |
return ` | |
<div class="${tocItemClassName} h${headingLevel}"> | |
<a href="#${getArticleHeadingId(article, headingNumber)}">${headingNumber}</a> | |
${' '} | |
${heading.textContent} | |
</div> | |
`; | |
} | |
} | |
function getArticleHeadingId(article, headingNumber) { | |
const headingNumberId = headingNumber.replace(/.$/, ''); | |
return `toc${getArticleId(article)}-${headingNumberId}`; | |
} | |
let articleIdProvider = {}; | |
let articleId = 1; | |
function getArticleId(article) { | |
if (typeof articleIdProvider[article] !== 'undefined') { | |
return articleIdProvider[article]; | |
} | |
return articleIdProvider[article] = articleId++; | |
} | |
/** | |
* | |
* @param {Element} article | |
* @param {*} headingTree | |
* @param {string} parentHeadingNumber | |
*/ | |
function injectHeadingNumber(article, headingTree, parentHeadingNumber) { | |
let internalNumber = 1; | |
for (const { heading, children } of headingTree) { | |
const headingNumber = `${parentHeadingNumber || ''}${internalNumber}.`; | |
heading.insertAdjacentHTML('afterbegin', createReverseHeadingAnchor(headingNumber)); | |
injectHeadingNumber(article, children, headingNumber); | |
internalNumber += 1; | |
} | |
/** | |
* @param {string} headingNumber | |
*/ | |
function createReverseHeadingAnchor(headingNumber) { | |
const headingId = getArticleHeadingId(article, headingNumber); | |
return ` | |
<a id="${headingId}" href="#${headingId}">${headingNumber}</a> | |
${' '} | |
`; | |
} | |
} | |
/** | |
* @param {Element[]} headings | |
*/ | |
function* buildHeadingTree(headings) { | |
if (headings.length < 1) { | |
return; | |
} | |
const [heading, ...nextHeadings] = headings; | |
const children = Array.from(takeWhile(nextHeadings, nextHeading => isChildHeading(nextHeading, heading))); | |
yield { | |
heading, | |
children: Array.from(buildHeadingTree(children)), | |
}; | |
const siblings = nextHeadings.slice(children.length); | |
yield* buildHeadingTree(siblings); | |
} | |
/** | |
* @param {Element} child | |
* @param {Element} parent | |
*/ | |
function isChildHeading(child, parent) { | |
const { nodeName: childLevel } = child; | |
const { nodeName: parentLevel } = parent; | |
return childLevel > parentLevel; | |
} | |
/** | |
* @param {T[]} array | |
* @param {Function<T, boolean>} predicate | |
*/ | |
function* takeWhile(array, predicate) { | |
for (const item of array) { | |
if (!predicate(item)) { | |
return; | |
} | |
yield item; | |
} | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment