'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}
`; // table(string header, string body) renderer.table = function(header, body) { // return `${code}`;
renderer.br = () => '/g, '').replace(/<\/p>/g, '');
if (text.startsWith('${text}`;
} else {
//--- 메뉴에는 되나 선택시 정상 동작하지 않음
return ``;
}
}
renderer.link = function(href, title, text) {
idxAll = idxAll + 1;
let txtParam = '';
let txtClass = `nav-link idxAll_${idxAll}_${menuGroupIdx + 1}_${menuIdx + 1}`;
menuIdx = menuIdx + 1;
if ((href.startsWith('record=')) && (options.file.endsWith(href + '.md'))) {
txtClass = txtClass + ' active';
} else if (href == options.file) {
txtClass = txtClass + ' active';
}
txtParam = `class="${txtClass}" style="font-size: 14px;" id="menu_${menuIdx}"`;
href = href.replace(/ /g, '%20');
if (href.startsWith('http')) {
return `${text}`;
} else if (href.startsWith('mailto:')) {
return `${text}`;
} else if (href.startsWith('tel:')) {
return `${text}`;
} else if (href.startsWith('#')) {
return `${text}`;
} else {
//--- To-Do : href="url|_blank" 형태로 처리
let target = '';
if (text.endsWith(' ')) {
target = 'target="_blank"';
text = text.trim();
}
if (href.endsWith('.md')) {
if (href.startsWith('/')) {
return `${text}`;
} else {
return `${text}`;
}
} else if (href.startsWith('record=')) {
return `${text}`;
} else {
if (href.startsWith('/')) {
return `${text}`;
} else {
return `${text}`;
}
}
}
}
let headingIndex = 0;
renderer.heading = function(text, level, raw, slugger) {
headingIndex = headingIndex + 1;
const uniqueId = `heading_${headingIndex}`;
if (level <= 2) {
return `
${text}
`; return renderer; } _getContentFromFile(filepath) { if ((filepath == null) || (fs.existsSync(filepath) == false)) { logger.error('File not found: ' + filepath); return ''; } return fs.readFileSync(filepath, "utf8"); } } module.exports = Markdown;