업무 기록/WEB

자바스크립트 이용한 마크다운 자동 목차 만들기

code2772 2023. 9. 7. 08:23
728x90
반응형

구현목표

내가 만들고자 하는 것은 마크업언어로 #in(textarea) 에 작성을 하면 #out html자동 변환되고 여기서 변환된 #out의 html 문법을 보고 목차를 자동으로 만드는게 목표이다. 작업을 하면서 구글에 많은 티스토리 블로그 자동 목차만들기를 활용해서 사용하면 되겠다고 생각하였다. 여기서는 자동목차 만들기만 확인할것이다.

변환 부분

html 기본 구조

<div id="in" style= "disply:none;">
        <form>
            <textarea id="code"  contenteditable= "true">
                # 뉴스 제목 1
                ## 부제목 1
                ### 소제목 1.1
                # 또 다른 주제
            </textarea>
        </form>
    </div>
    <div id="out" class="markdown-body"></div>
    <div id="tree"></div>
      <div id="menu">
        <span>다운로드</span>
        <div id="saveas-pdf">
          <span>Markdown</span>
        </div>
        <div id="saveas-markdown">
          <span>Markdown</span>
        </div>
        <div id="saveas-html">
          <span>HTML</span>
        </div>
        <a id="close-menu">&times;</a>>
      </div>

위는 기본적인 html의 뼈대이다. 생략된 css난 html 부분이 많이있다. (참고용)

 

 

결과페이지 미리보기

결과 페이지 목차

자바스크립트 전체

$(document).ready(function () {
    // 여기에 Markdown 콘텐츠를 HTML로 변환하고 #out 요소를 업데이트하는 코드 추가

    // 목차 플러그인 초기화
    $("#tree").toc({
        content: "#out", // HTML 콘텐츠는 표시할 요소의 ID와 일치해야 합니다.
        headings: "h1,h2,h3", // Markdown 제목 구조에 맞게 사용자 정의
        indent: true,
        indentBy: " ",
        level: function (element) {
            var tagName = element.tagName.toLowerCase();
            if (tagName == "h1") return 0;
            if (tagName == "h2") return 1;
            if (tagName == "h3") return 2;
            return 0;
        }
    });
});

(function ($) {
    "use strict";

    // 현재 선택한 요소의 목차를 포함한 목록을 작성합니다.
    // 옵션:
    // content: 목차를 찾을 콘텐츠
    // headings: 상대적인 계층 구조 순서대로 나열된 쉼표로 구분된 선택기 문자열
    var toc = function (options) {
        return this.each(function () {
            var root = $(this),
                data = root.data(),
                thisOptions,
                stack = [root], // 상위로 올라간 역순 스택은 목록 요소를 추적합니다.
                listTag = this.tagName;
            currentLevel = 0;
            headingSelectors;

            // 기본값: 플러그인 매개변수는 데이터 속성을 재정의하고 기본값을 재정의합니다.
            thisOptions = $.extend(
                { content: "body", headings: "h1,h2,h3" },
                { content: data.toc || undefined, headings: data.tocHeadings || undefined },
                options
            );
            headingSelectors = thisOptions.headings.split(",");

            // 이미 ID가 없는 경우 몇 가지 자동 ID를 설정합니다.
            $(thisOptions.content)
                .find(thisOptions.headings)
                .attr("id", function (index, attr) {
                    // HTML5에서는 ID 속성은 하나 이상의 문자를 가져야 하며 공백 문자를 포함해서는 안됩니다.
                    //
                    // 모든 브라우저가 이것을 처리하므로 이제 HTML5 사양을 사용합니다.
                    // https://mathiasbynens.be/notes/html5-id-class
                    var generateUniqueId = function (text) {
                        // 유효한 ID를 생성합니다. 공백은 밑줄로 대체됩니다. 또한 문서에서 이미 ID가 있는지 확인합니다.
                        // 그렇다면 "_1", "_2" 등을 추가합니다.
                        //사용되지 않은 ID를 찾을 때까지.
                        if (text.length === 0) {
                            text = "?";
                        }

                        var baseId = text.replace(/\s+/g, "_"),
                            suffix = "",
                            count = 1;

                        while (document.getElementById(baseId + suffix) !== null) {
                            suffix = "_" + count++;
                        }

                        return baseId + suffix;
                    };

                    return attr || generateUniqueId($(this).text());
                })
                .each(function () {
                    //현재 제목의 수준은 무엇인가요?
                    var elem = $(this),
                        level = $.map(headingSelectors, function (selector, index) {
                            return elem.is(selector) ? index : undefined;
                        })[0];

                    if (level > currentLevel) {
                        // 제목이 현재 수준보다 더 깊은 수준에 있다면 새로운 중첩 목록을 시작합니다.
                        // 단, 상위에 이미 몇 개의 목록 항목이 있는 경우에만 수행합니다.
                        // 그렇지 않으면 수준을 건너뛰고 현재 수준에 새 목록 항목을 추가할 수 있습니다.
                        // 상위 목록에 이미 몇 개의 목록 항목이 있는지 확인하는 것입니다.
                        // 역방향 스택에서 unshift = push이며 stack[0] = 상단입니다.
                        var parentItem = stack[0].children("li:last")[0];
                        if (parentItem) {
                            stack.unshift($("<" + listTag + "/>").appendTo(parentItem));
                        }
                    } else {
                        // 스택을 현재 수준에 맞게 자르기 위해 스택의 맨 위에서부터 일부를 잘라내기 위해 splice를 사용합니다.
                        // 또한 스택에 적어도 하나의 요소를 유지해야 합니다. 즉, 포함 요소입니다.
                        stack.splice(0, Math.min(currentLevel - level, Math.max(stack.length - 1, 0)));
                    }

                    //목록 항목 추가
                    $("<li/>")
                        .appendTo(stack[0])
                        .append($("<a/>").text(elem.text()).attr("href", "#" + elem.attr("id")));

                    currentLevel = level;
                });
        });
    },
        old = $.fn.toc;

    $.fn.toc = toc;

    $.fn.toc.noConflict = function () {
        $.fn.toc = old;
        return this;
    };

    // Data API
    $(function () {
        toc.call($("[data-toc]"));
    });
})(window.jQuery));

먼저 마크업으로 작성한 #in 에서 #out에 html로 변환한 후 목차를 h1, h2, h3 태그로 변환된 내용을 목차로 자동으로 만든 부분이다. 더 자세하게 설명하면 내가 #in부분에 "# 안녕" 이라고 작성을하면 <h1>안녕<h1/>으로  # out에 자동으로 변환되고 이를 통해 목차를 자동으로 만들어 준다.

 

반응형

 

자바스크립트 세부설명

문서준비 함수

$(document).ready(function () {
    // Markdown 내용을 HTML로 변환하고 #out 요소를 업데이트하는 코드를 여기에 작성합니다.

    // 목차 플러그인 초기화
    $("#tree").toc({
        content: "#out", // HTML 컨텐츠는 보여질 요소의 ID와 일치해야 합니다.
        headings: "h1,h2,h3", // Markdown 제목 구조에 맞게 수정하세요.
        indent: true; // 들여쓰기 활성화
        indentBy : " "; // 들여쓰기에 사용할 문자(여기서는 공백)
        level: function (element){
            var tagName = element.tagName.toLowerCase();
            if(tagName =="h1") return 0;
            if(tagName =="h2") return 1;
            if(tagName =="h3") return 2;
            return 0;
        }
    });
});

html 페이지가 로드된 시점에서 실행된다. tagName이 h1, h2, h3 에 따라 들여쓰기를 활성화하고 구분을 공백으로 작업할려고 하였다. 

 

목차 플러그인(플러그인 이름은 사용자 정의 가능) 초기화:

$("#tree").toc({
    // 목차 플러그인의 구성 옵션들을 정의합니다.
});

목차 플러그인 초기화의 주요 이유는 웹 페이지나 문서의 긴 내용을 보다 쉽게 탐색하고 이해하기 위함으로 이 부분은 ID가 "tree"인 HTML 요소에 목차 플러그인을 초기화하였다.

 

중첩된 목록을 생성하거나 스택에서 항목을 제거

if (level > currentLevel) {
                        // 제목이 현재 수준보다 더 깊은 수준에 있다면 새로운 중첩 목록을 시작합니다.
                        // 단, 상위에 이미 몇 개의 목록 항목이 있는 경우에만 수행합니다.
                        // 그렇지 않으면 수준을 건너뛰고 현재 수준에 새 목록 항목을 추가할 수 있습니다.
                        // 상위 목록에 이미 몇 개의 목록 항목이 있는지 확인하는 것입니다.
                        // 역방향 스택에서 unshift = push이며 stack[0] = 상단입니다.
                        var parentItem = stack[0].children("li:last")[0];
                        if (parentItem) {
                            stack.unshift($("<" + listTag + "/>").appendTo(parentItem));
                        }
                    } else {
                        // 스택을 현재 수준에 맞게 자르기 위해 스택의 맨 위에서부터 일부를 잘라내기 위해 splice를 사용합니다.
                        // 또한 스택에 적어도 하나의 요소를 유지해야 합니다. 즉, 포함 요소입니다.
                        stack.splice(0, Math.min(currentLevel - level, Math.max(stack.length - 1, 0)));
                    }

제목 요소의 수준을 기반으로 중첩된 목록을 생성하거나 스택을 조정합니다. 현재 수준과 비교하여 중첩된 목록을 생성하거나 스택에서 항목을 제거

 

목록 이동

$("<li/>")
    .appendTo(stack[0])
    .append($("<a/>").text(elem.text()).attr("href", "#" + elem.attr("id")));

생성된 목차에 각 항목을 추가합니다. li 요소를 생성하고, 해당 항목의 텍스트와 링크를 설정하여 목차에 포함

 

또하나의 목적은 h1, h2, h3 목차 구분을 위해서 들여쓰기를 통해 시각화를 할려했지만 테스트에서는 잘 적용되었는데 실제 내가 만들고 있는 사이트에서 js, css등이 많아 겹치는 느낌이 있어 적용이 안되었다. 근데 막상보니 정렬되면 공간이 부족할거 같아 그냥 변경을 포기했다.

 

수정

 

 if (parentItem) {
    stack.unshift($("<" + listTag + "/>").appendTo(parentItem)
    .css("margin-left", "10px");
                }

.css("margin-left", "10px") 적용을 안했던 문제였다. 이전에는 왜 작동을 했는지 모르겠지만 내가 값을 빼먹은거같다. 이 후 정상작동을 한다.

반응형