'use strict' /** * Copyright (c) 2017~2022, OBCon Inc. * All rights reserved. */ /** * @file * @copyright 2017~2022, OBCon Inc. * @author gye hyun james kim [pnuskgh@gmail.com] */ const fs = require('fs'); const path = require('path'); const marked = require('marked'); const highlightjs = require('highlightjs'); //--- https://github.com/markedjs/marked //--- https://marked.js.org/ //--- https://highlightjs.org/ //--- https://www.hahwul.com/2019/02/editorjs-markdown-javascript-library.html //--- Markdown 모듈과 openapi/deploy.js 파일에서 사용중 class Markdown { constructor() { this._isDebug = false; this._options = null; //--- 옵션 this._file = null; //--- Markdown 파일 this._config = null; //--- Markdown 파일을 위한 설정 this._content = null; //--- Markdown 파일 내용 this._baseUrl = null; //--- Markdown 파일의 baseUrl this._service = null; //--- 강제로 지정한 서비스 이름 } debug(...messages) { if (this._isDebug) { console.log(...messages) } } initialize(options={}) { this._options = Object.assign({ req: null, //--- Express.Request res: null, //--- Express.Response language: 'ko_KR', //--- 사용 언어 file: 'README.md', //--- Markdown 파일 service: null //--- 강제로 지정한 서비스 이름 }, options); this._file = null; //--- Markdown 파일 this._config = null; //--- Markdown 파일을 위한 설정 this._content = null; //--- Markdown 파일 내용 this._baseUrl = null; //--- Markdown 파일의 baseUrl this._service = null; //--- 강제로 지정한 서비스 이름 } get options() { return this._options; } get file() { if (this._file == null) { this._file = this._getFile(); } return this._file; } //--- Markdown 파일 구조 //--- this.options.file = /${folder}/${file}.md //--- manual/${service}/site_${siteID}/${lang}/this.options.file //--- manual/${service}/site_${site}/ko_KR/this.options.file //--- manual/${service}/default/${lang}/this.options.file //--- manual/${service}/default/ko_KR/this.options.file _getFile() { const file = this.options.file; const files = []; if ((this.options.req) && (this.options.req.session) && (this.options.req.session.isLogined)) { files.push(utils.getPath('manual', config.service.name, 'site_' + this.options.req.session.site.id, this.options.language, file)); files.push(utils.getPath('manual', config.service.name, 'site_' + this.options.req.session.site.id, 'ko_KR', file)); files.push(utils.getPath('manual', config.service.name, 'default', this.options.language, file)); files.push(utils.getPath('manual', config.service.name, 'default', 'ko_KR', file)); } else { files.push(utils.getPath('manual', config.service.name, 'default', this.options.language, file)); files.push(utils.getPath('manual', config.service.name, 'default', 'ko_KR', file)); } for (let idx = 0; idx < files.length; idx++) { if (fs.existsSync(files[idx])) { return files[idx].replace(/\\/g, '/'); } } return files[0].replace(/\\/g, '/'); } //--- Menu 파일 종류 //--- 로그인 전 : MENU_PUBLIC.md //--- 로그인 후 : MENU_PRIVATE.md //--- Menu 파일 구조 //--- 적용할 메뉴 파일 : 발견한 Markdown 파일을 기준으로 메뉴를 적용 한다. //--- folder(this._file)/${menufile}.md, 상위 폴더로 순환 _getMenuFile() { let menuFile = ((this.options.req != null) && (this.options.req.session.isLogined)) ? 'MENU_PRIVATE.md':'MENU_PUBLIC.md'; menuFile = this.config.menuFile || menuFile; const files = []; const manualFolder = path.join(config.folder_root, 'manual', config.service.name); let menuFolder = path.dirname(this.file); while (manualFolder.length <= menuFolder.length) { files.push(path.join(menuFolder, menuFile)); menuFolder = path.dirname(menuFolder); } let rtMenuFile = files[0]; for (let idx = 0; idx < files.length; idx++) { if (fs.existsSync(files[idx])) { rtMenuFile = files[idx] break; } } return rtMenuFile; } get config() { if (this._config == null) { this._config = this._getConfig_new(); // this._config = this._getConfig(); } return this._config; } //--- 사용하지 않음 // _getConfig() { // let rtConfig = this._getDefaultConfig(); // [ // path.join(path.dirname(this.file), 'config.js'), // path.join(path.dirname(this.file), path.basename(this.file, '.md') + '.js') // ].forEach(file => { // if (fs.existsSync(file)) { // console.log('getConfig: file exist'); // delete require.cache[file]; // const conf = require(file); // Object.assign(rtConfig, conf); // } // }); // return rtConfig; // } _getConfig_new() { const file = path.basename(this.options.file, '.md') + '.js'; const files = []; if ((this.options.req) && (this.options.req.session) && (this.options.req.session.isLogined)) { files.push(utils.getPath('manual', config.service.name, 'site_' + this.options.req.session.site.id, this.options.language, file)); files.push(utils.getPath('manual', config.service.name, 'site_' + this.options.req.session.site.id, this.options.language, 'config.js')); files.push(utils.getPath('manual', config.service.name, 'site_' + this.options.req.session.site.id, 'ko_KR', file)); files.push(utils.getPath('manual', config.service.name, 'site_' + this.options.req.session.site.id, 'ko_KR', 'config.js')); files.push(utils.getPath('manual', config.service.name, 'default', this.options.language, file)); files.push(utils.getPath('manual', config.service.name, 'default', this.options.language, 'config.js')); files.push(utils.getPath('manual', config.service.name, 'default', 'ko_KR', file)); files.push(utils.getPath('manual', config.service.name, 'default', 'ko_KR', 'config.js')); } else { files.push(utils.getPath('manual', config.service.name, 'default', this.options.language, file)); files.push(utils.getPath('manual', config.service.name, 'default', this.options.language, 'config.js')); files.push(utils.getPath('manual', config.service.name, 'default', 'ko_KR', file)); files.push(utils.getPath('manual', config.service.name, 'default', 'ko_KR', 'config.js')); } let rtConfig = this._getDefaultConfig(); for (let idx = 0; idx < files.length; idx++) { const file = files[idx]; if (fs.existsSync(file)) { // console.log('getConfig: file exist', file); delete require.cache[file]; const conf = require(file); Object.assign(rtConfig, conf); break; } } return rtConfig; } _getDefaultConfig() { return { layout: config.theme.layout, //--- layout isPublic: false, //--- false. 로그인 필요 menuFile: null, //--- 메뉴 파일 showIndex: true, //--- Reserved: 페이지 상단에 목차 표시 (Default. true) prev: null, //--- Reserved: 이전 페이지 (Default. null) next: null //--- Reserved: 다음 페이지 (Default. null) }; } get content() { if (this._content == null) { this._content = this._getContent(); } return this._content; } _getContent() { if ((this._file == null) || (fs.existsSync(this._file) == false)) { logger.error('File not found: ' + this._file); return ''; } else { return fs.readFileSync(this._file, "utf8"); } } get baseUrl() { if (this._baseUrl == null) { this._baseUrl = this._getBaseUrl(); } return this._baseUrl; } _getBaseUrl() { let baseUrl = this.file; baseUrl = baseUrl.substring(baseUrl.indexOf('/manual')); baseUrl = baseUrl.substring(0, baseUrl.length - this.options.file.length); return utils.link.join(config.http.path, baseUrl); } get service() { if (this._service == null) { this._service = this._getService(); } return this._service; } _getService() { let service = this.options.service || ''; if (service == '') { //--- Parameter로 전달된 servcie 값이 없으면 세션에 저장된 service 값을 사용 한다. //--- service 값이 없어도 이전에 지정한 service 값으로 화면을 조회 한다. if ((this.options.req) && (this.options.req.session) && (this.options.req.session.service)) { service = this.options.req.session.service; } } if (service == '') { service = config.service.name; } //--- 세션에 service 이름을 저장 한다. if ((this.options.req) && (this.options.req.session) && (this.options.req.session.service != service)) { this.options.req.session.service = service; this.options.req.session.save(function(err) { }); } return service; } // //--- Markdown 파일을 HTML 페이지로 변환하여 반환 한다. // // options = { // // target: 표시할 Content 지정 (menu, source, content) // // format: 변환한 후의 양식 (html, pdf(Reserved)) // // content: target이 content일 때, markdown 내용을 담은 문자열 // // filename: format이 pdf일 때, 저장할 파일 이름 // // } // getHtml() { // // const parserHtml = this._getHtmlParser({ // // baseUrl: this.baseUrl, // // folder: path.dirname(this.options.file) // // }); // // //--- http://local.bluestones.biz:90/cms/manual/service_cms_bluestone/default/ko_KR/${config.http.path}/invests // // const baseUrl = config.http.baseUrl + config.http.path + `/manual/${this.service}/${this.options.language}`; // // this._setMarkedOptions(baseUrl, parserHtml); // // return this.parseMarkdown(this.content); // return this.convert({ // baseUrl: this.baseUrl, // folder: path.dirname(this.options.file) // }); // } // options = { // source: ~ // content: ~ // target: ~ // } convert(options={}) { options = Object.assign({ language: this.options.language, //--- 언어 (ko_KR, en_US, ...) folder: path.dirname(this.options.file), //--- Markdown 파일의 폴더 file: this.options.file, //--- Markdown 파일 이름 source: options.source || 'file', //--- 원본 : file. file 사용, menu. 메뉴 file 사용, content. content 사용 content: options.content || '', //--- target이 content인 경우 markdown 문서를 저장 //--- 변환 : html. HTML 반환, index. 인덱스 반환, menu. 메뉴 HTML 반환, info: 정보 반환 target: options.target || 'html', baseUrl: options.baseUrl || this.baseUrl //--- link와 image에서 사용하여 prefix URL // //--- Target 양식 : source. 원본 반환, index. 인덱스 반환, menu. 메뉴 반환, content. ??? // target: options.target || 'source', //--- Output 포맷 : html. HTML로 반환. pdf. PDF로 변환 (Reserved) // format: options.format || 'html', // //--- format이 pdf일 때, 저장할 파일 이름 (Reserved) // // filename: options.filename || 'noname.pdf', }, options); // const baseUrl = config.http.baseUrl + config.http.path + '/manual/' + config.service.name + '/' + this.options.language; const baseUrl = utils.link.join(config.http.baseUrl, config.http.path, 'manual', config.service.name, + this.options.language); switch(options.target) { case 'index': //--- Markdown Page Index this._setMarkedOptions(baseUrl, this._getIndexParser(options)); break; case 'menu': //--- 문서 목차 this._setMarkedOptions(baseUrl, this._getMenuParser(options)); break; case 'html': //--- Markdown을 HTML로 변환 this._setMarkedOptions(baseUrl, this._getHtmlParser(options)); break; case 'info': return { filename: this.file }; break; default: break; } const content = this.getContent(options); return this._parseMarkdown(modules.markdowns.lang.replaceContent(content, this.service)); } getContent(options) { switch (options.source) { case 'content': return options.content; case 'menu': return this._getContentFromFile(this._getMenuFile()); case 'file': default: return this._getContentFromFile(this.file); } } //--- https://marked.js.org/using_advanced //--- https://marked.js.org/using_pro _setMarkedOptions(baseUrl, renderer) { marked.setOptions({ baseUrl: baseUrl.replace('//', '/'), renderer: renderer, highlight: function(code, lang) { // const language = highlightjs.getLanguage(lang) ? lang : 'plaintext'; // return highlightjs.highlight(code, { language }).value; return highlightjs.highlightAuto(code).value; }, langPrefix: 'language-', //--- code에서 언어의 prefix breaks: false, //--- true. \n을
로 표시 한다. gfm: true, headerIds: true, headerPrefix: '', mangle: true, pedantic: false, sanitize: false, sanitizer: null, silent: false, smartLists: true, smartypants: false, xhtml: false }); } _parseMarkdown(content) { let rtContent = content.replace(/\r\n/g, '\n'); rtContent = (typeof(marked.marked) == 'undefined') ? marked(rtContent):marked.marked(rtContent); //--- To-Do : windows 환경에서 marked의 경우 다음 오류가 발생 한다. //--- "## Sitemap\n \n" #--- 비정상 표시 //--- "## Sitemap\n\n" #--- 정상 표시 // const contentPre = (typeof(marked.marked) == 'undefined') ? marked(this._contentPre(content)):marked.marked(this._contentPre(content)); // if (format == 'html') { // rtContent = rtContent.replace(/\n \n -/g, '\n -'); // //--- ' \n'을 '
\n'으로 변환 한다. // return rtContent.replace(/ \n/g, '
\n'); // } return rtContent; } _getNoneParser() { const renderer = new marked.Renderer(); const funcNone = () => ''; renderer.code = funcNone; renderer.blockquote = funcNone; renderer.html = funcNone; renderer.heading = funcNone; renderer.hr = funcNone; renderer.list = funcNone; renderer.listitem = funcNone; renderer.checkbox = funcNone; renderer.paragraph = funcNone; renderer.table = funcNone; renderer.tablerow = funcNone; renderer.tablecell = funcNone; renderer.strong = (text) => `${text}`; renderer.em = funcNone; renderer.codespan = funcNone; renderer.br = funcNone; renderer.del = funcNone; renderer.link = funcNone; renderer.image = funcNone; renderer.text = (text) => text; return renderer; } //--- Web_Publishing.md 파일 참조 _getHtmlParser(options) { const debug = this.debug.bind(this); const renderer = new marked.Renderer(); //--- Block level renderer methods // code(string code, string infostring, boolean escaped) //--- infostring : 사용 언어 renderer.code = (code, infostring, escaped) => `
${code}
`; renderer.blockquote = (quote) => `
${quote}
`; renderer.html = (html) => html; let headingIndex = 0; // heading(string text, number level, string raw, Slugger slugger) renderer.heading = function(text, level, raw, slugger) { headingIndex = headingIndex + 1; // const uniqueId = slugger.slug(text); const uniqueId = `heading_${headingIndex}`; // return `${text}`; if (level <= 2) { return `
${text}
`; } else { return `
${text}`; } } renderer.hr = () => '
'; // list(string body, boolean ordered, number start) let index_list = 0; renderer.list = function(body, ordered, start) { index_list = index_list + 1; if (ordered) { //---
    return `
      ${body}
    `; } else { //---