본문 바로가기
업무 기록/WEB

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

by code2772 2023. 9. 7.

[ 목차 ]

    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") 적용을 안했던 문제였다. 이전에는 왜 작동을 했는지 모르겠지만 내가 값을 빼먹은거같다. 이 후 정상작동을 한다.

    반응형