oda-framework
Version:
368 lines (366 loc) • 19.6 kB
JavaScript
let path = import.meta.url;
let mdShowdown;
ODA({ is: 'oda-md-viewer', imports: './dist/showdown.min.js, ./dist/decodeHTML.min.js, ./dist/highlight.min.js',
template: `
<style>
p { margin: {{pmargin || 'unset'}}; }
:host { display: block; height: 100%; overflow-y: auto; line-height: 1.67;}
table { border-collapse: collapse; width: 100%; margin-bottom: 16px;}
th { @apply --header; border: 1px solid darkgray; padding: 2px; }
td { border: 1px solid lightgray; padding: 2px; }
img { max-width: 96%; height: auto; }
blockquote { border-left: 10px solid #ccc; background: #f9f9f9; padding: 0.5em 10px; margin: 1.5em 10px; }
code { background-color: #f8f8f8; border: 1px solid #dfdfdf; color: #333; font-family: Consolas,"Liberation Mono",Courier,monospace; font-weight: normal; padding: 0.125rem 0.3125rem 0.0625rem;}
/*.katex-html { display: none; }*/
</style>
<div ~html="html" style="margin: 2px 10px; white-space: normal;"></div>
`,
set srcmd(n) {
this.setHTML(n);
},
set src(n) {
if (!this.srcmd) this.setHTML(n);
},
html: '',
$public: {
pmargin: ''
},
options: {},
defaultOptions: {
omitExtraWLInCodeBlocks: false, noHeaderId: false, prefixHeaderId: false, rawPrefixHeaderId: false, ghCompatibleHeaderId: false, rawHeaderId: false, headerLevelStart: false, parseImgDimensions: false, simplifiedAutoLink: false, literalMidWordUnderscores: false, literalMidWordAsterisks: false, strikethrough: false, tables: true, tablesHeaderId: false, ghCodeBlocks: true, tasklists: false, smoothLivePreview: false, smartIndentationFix: false, disableForced4SpacesIndentedSublists: true, simpleLineBreaks: false, requireSpaceBeforeHeadingText: false, ghMentions: false, ghMentionsLink: 'https://github.com/{u}', encodeEmails: true, openLinksInNewWindow: true, backslashEscapesHTMLTags: true, emoji: true, underline: true, completeHTMLDocument: false, metadata: false, splitAdjacentBlockquotes: false
},
async initShowdown() {
if (!mdShowdown)
// await import('./dist/showdown-katex.js'); // https://github.com/obedm503/showdown-katex
await import('./dist/showdown-youtube.min.js');
mdShowdown = new showdown.Converter({
...this.defaultOptions, ...this.options,
extensions: ['youtube',
// showdownKatex({
// delimiters: [
// { left: "$", right: "$", display: false },
// { left: '~', right: '~', display: false, asciimath: true },
// ],
// })
, () => {
return [{
type: "output",
filter(text) {
let left = "<pre><code\\b[^>]*>", right = "</code></pre>", flags = "g";
const replacement = (wholeMatch, match, left) => {
let lang = (left.match(/class=\"([^ \"]+)/) || [])[1];
let html = lang && hljs.getLanguage(lang) ? hljs.highlight(lang, htmlDecode(match)).value : hljs.highlightAuto(htmlDecode(match)).value;
let encode = htmlEncode(match);
return `<oda-md-code code="${encode}" lang="${lang}"></oda-md-code>`;
};
return showdown.helper.replaceRecursiveRegExp(text, replacement, left, right, flags);
}
}]
}]
})
},
async attached() {
if (!this.src && !this.srcmd) {
let url = new URL(window.location);
let s = url.searchParams.get("lzs");
if (s) {
const lzs = path.replaceAll('components/viewers/md-viewer/md-viewer.js', 'tools/jupyter/lib/lz-string.js');
if (!this.LZString) {
const { LZString } = await import(lzs);
this.LZString = LZString;
}
s = this.LZString.decompressFromEncodedURIComponent(s);
this.setHTML('', s);
} else {
s = url.searchParams.get("s");
this.setHTML(s);
}
}
},
async setHTML(s, lzs) {
if (s || lzs) {
await this.initShowdown();
let src = lzs || s;
if (!lzs && (!this.srcmd && (s.endsWith('.md') || !(s.includes('~~~') || s.includes('```'))))) {
try {
src = await fetch(s);
src = src && src.ok ? await src.text() : s;
} catch (e) { }
}
this.source = src || s;
// src = src.replace(/\$\$(.*)\$\$/g, `<h2 style="text-align: center;">$ $1 $</h2>`);
src = src.replace('~~~~~_~~~~~', '');
src = src.replace(/(```\S*|~~~\S*)( +)/g, '$1' + '_');
this.html = mdShowdown.makeHtml(src);
}
}
})
let files = new Map();
let fileCount = 0;
ODA({ is: 'oda-md-code', imports: '@oda/icon, @oda/code-editor',
template: `
<style>
:host { transition: opacity .5s linear, visibility .5s linear; }
.md { @apply --layout; display: block; padding: 4px; }
.hljs { @apply --layout; display: block; padding: {{md ? '8px 4px 6px 10px;' : ''}}; height: {{_eh ? _eh + 'px' : ''}} }
.hljs-comment, .hljs-quote { color: #93a1a1; }
.hljs-keyword, .hljs-selector-tag, .hljs-addition { color: #859900; }
.hljs-number, .hljs-string, hljs-meta .hljs-meta-string, .hljs-literal, .hljs-doctag, .hljs-regexp { color: #2aa198; }
.hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #268bd2; }
.hljs-attribute, .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-class .hljs-title, .hljs-type { color: #b58900; }
.hljs-symbol, .hljs-bullet, .hljs-subst, .hljs-meta, .hljs-meta .hljs-keyword, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-link { color: #cb4b16; }
.hljs-built_in, .hljs-deletion { color: #dc322f; }
.hljs-formula { background: #eee8d5; }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: bold; }
.hjln { min-width:34px; color:gray; border-right:.1em solid; counter-reset: l; cursor:default; float:left; padding:8px 0; margin:0 0.5em 0 0; text-align:right; -moz-user-select:none; -webkit-user-select:none }
.hjln span { counter-increment:l; display:block;padding:0 .5em 0 1em }
.hjln span:before { content:counter(l) }
.light { background-color: #ddd; display: inline-block;}
.icon-info { display: flex; }
.copy:active:before { position: absolute; right: 0; top: -26px; color: red; content:"copied"; }
</style>
<div class="flex vertical" ~style="{'margin-left': infpnl?'10px':''}" style=" position: relative;padding:0;color:darkgray">
<div ~if="showFilename" style="font-size:.8em;position: absolute; top:-4px;left:2px;">{{filename}}</div>
<div style="cursor:pointer;position: absolute; top:18px;right:4px;display: flex">
<oda-icon ~if="showRun && showConsole" icon="device:developer-mode" icon-size="16" style="cursor:pointer; z-index: 9;" @tap="_switchConsole" title="show console"></oda-icon>
<oda-icon class="copy" ~if="bcopy" icon="icons:content-copy" icon-size="16" style="cursor:pointer; z-index: 9;" @tap="_copy" title="copy to clipboard"></oda-icon>
</div>
<pre class="hljs" style="display:flex;border: .5px solid darkgray;border-radius:2px;overflow:auto;overflow-x:auto;min-height:32px"
~style="line? '' : 'white-space:pre-wrap;'+_getColor(true)">
<oda-icon class="icon-info" ~class="infpnl" ~style="_getColor" :icon="_getInfoIcon" style="position:absolute;left:-11px;top:22px;background:white; border-radius: 50%; z-index: 9"
~if="infpnl && !hideicon" :title="infpnl"></oda-icon>
<code ~if="md" :contenteditable="enableEdit" ~class="md?'md':'hljs'" ~html="html" @input="_changed" style="outline:0px solid transparent;"
~style="md?'font-family:var(--font-family);font-size:.95em':'font-size:1.25em;line-height: 1.3em;'"></code>
<oda-code-editor ~if="!md" :show-gutter="!hideGutter" :read-only="!enableEdit" :show-print-margin="false" :src="_code" style="flex: 1; background-color: white; opacity: unset !important; filter: unset !important;" font-size="16" :mode="_mode" @change="_changed" ~style="{'padding-left': hideGutter ? '10px' : '0', 'padding-top': hideGutter ? '16px' : 0 }" highlight-active-line="false" min-lines="1" :max-lines="_maxLines || 100" show-cursor=false @wheel.stop></oda-code-editor>
</pre>
<iframe ~if="showRun" :srcdoc="_src" style="padding: 6px;border-radius:0 0 2px 2px;border: 1px solid darkgray;min-width:0px;height:28px;margin:-14px 0 14px 0"
onload="setTimeout(()=>{this.style.height=this?.contentDocument?.body?.scrollHeight+'px'},500)" ~style="{'min-height': (_h||28)+'px','max-height': _h ? _h+'px':'unset'}"></iframe>
<div ~if="showRun && _showConsole" style="display:flex;margin-top:-14px">
<div style="font-size:.8em;">console:</div>
<div style="flex:1"></div>
<oda-icon icon="files:document" icon-size="12" style="z-index: 9; cursor:pointer;" @tap="_console=''" title="clear"></oda-icon>
<oda-icon icon="icons:refresh" icon-size="16" style="z-index: 9; cursor:pointer;" @tap="_console='';this._changed(false)" title="refresh"></oda-icon>
<oda-icon icon="icons:close" icon-size="16" style="z-index: 9; cursor:pointer;" @tap="_switchConsole" title="close"></oda-icon>
</div>
<textarea ~if="showRun && _showConsole" style="background-color:#eee;padding: 2px;border-radius:0 0 2px 2px;border: 1px solid darkgray;min-width:100%;max-width:100%;height:128px;"
readonly>{{_console}}</textarea>
</div>
`,
$public: {
code: String,
lang: String,
html: '',
_iframe: '',
_code: '',
hideicon: false,
hideGutter: false,
bcopy: true,
_show: false,
enableEdit: false,
showRun: false,
line: false,
md: false,
infpnl: '',
filename: '',
showFilename: false,
components: [],
_edit: '',
_lang: '',
_h: '',
_eh: '',
_src: '',
_loadODA: false,
showConsole: false,
_showConsole: false,
_console: '',
warn: false,
error: false,
_mode: '',
_maxLines: '100'
},
$observers: {
init(code, lang) {
this._code = htmlDecode(code);
if (this.md) {
this.html = mdShowdown.makeHtml(this._code).replace(/<p>|<\/p>/g, '');
} else {
this.html = this._code;
}
let n = lang;
this._lang = lang;
let _lang = '';
let langs = ['javascript', 'xml', 'xquery', 'css', 'http', 'ini', 'json', 'html', 'python'];
langs.forEach(e => {
if (this._lang.startsWith(e)) _lang = e;
})
if (!_lang) {
langs.forEach(e => {
if (this._lang.includes(e)) _lang = e;
})
}
this._mode = _lang;
if (this.filename) {
this.showFilename = true;
} else {
fileCount += 1;
this.filename = 'file_' + fileCount + this._lang.includes('html') ? '.html' : this._lang.includes('javascript') ? '.js' : '';
}
this.bcopy = n.includes('copy') && !n.includes('nocopy');
this.enableEdit = n.includes('edit');
this.showRun = n.includes('run');
this.hideicon = n.includes('hideicon');
this.hideGutter = n.includes('hideGutter');
this.line = n.includes('line');
this.md = n.includes('md');
this.filename = n.includes('[') && n.includes(']') ? /(?:\[)(.*)(?:\])/g.exec(n)[1] : '';
this.components = n.includes('{') && n.includes('}') ? /(?:\{)(.*)(?:\})/g.exec(n)[1].split('_') : [];
['error', 'success', 'info', 'warning', 'help', 'like', 'faq'].forEach(e => { if (this._lang.includes(e)) this.infpnl = e });
this._h = /_h=(.*?)_/g.exec(n) ? /_h=(.*?)_/g.exec(n)[1] : '';
this._eh = /_eh=(.*?)_/g.exec(n) ? /_eh=(.*?)_/g.exec(n)[1] : '';
this._maxLines = /_maxLines=(.*?)_/g.exec(n) ? /_maxLines=(.*?)_/g.exec(n)[1] : '100';
this._loadODA = n.includes('loadoda');
this.showConsole = n.includes('console');
this.warn = n.includes('warn');
this.error = n.includes('error');
if (this.md) {
this.html = mdShowdown.makeHtml(this._code).replace(/<p>|<\/p>/g, '');
} else {
this.html = this._code;
}
files.set(this.filename, this._code);
this._getIframe();
if (this.components?.length) {
document.addEventListener("iframeChanged", e => {
if (this._lang !== e.detail._lang && this.components.includes(e.detail.filename)) {
this._changed(false);
}
});
}
}
},
_switchConsole() {
this._showConsole = !this._showConsole;
if (this._showConsole) this._changed(false);
else this._console = '';
},
_setConsole(e) {
if (this._showConsole && this.$('iframe')) {
const _cons = (e) => {
if (!this.warn && e.startsWith('console.warn > ')) return;
if (!this.error && e.startsWith('console.error > ')) return;
this._console += '\r\n' + e;
if (this.$('textarea')) {
let textarea = this.$('textarea');
this.async(() => {
textarea.scrollTop = textarea.scrollHeight;
})
}
}
let cons = (function(oldCons) {
return {
log: function(text) {
oldCons.log(text);
if (text.toString().startsWith('from alert')) _cons(text);
else _cons('console.log > ' + text);
},
info: function(text) {
oldCons.info(text);
_cons('console.info > ' + text);
},
warn: function(text) {
oldCons.warn(text);
_cons('console.warn > ' + text);
},
error: function(text) {
oldCons.error(text);
_cons('console.error > ' + text);
}
}
}(this.$('iframe').contentWindow.console));
this.$('iframe').contentWindow.console = cons;
}
},
async _copy() {
await navigator.clipboard.writeText(this._code);
},
_changed(iframeChanged = true) {
const code = this.$('code') || this.$('oda-code-editor');
if (!code) return;
this._edit = code.editor?.getValue() || code.innerText || '';
files.set(this.filename, this._edit);
this._getIframe();
if (iframeChanged && this.filename) document.dispatchEvent(new CustomEvent('iframeChanged', { detail: { _lang: this._lang, filename: this.filename } }));
},
_getIframe() {
if (!this.showRun || !this._code) return;
let css = '';
let code = files.get(this.filename) && files.get(this.filename);
if (this.filename.includes('.js') || this._lang.includes('javascript')) {
if (this.components && this.components.length) {
for (const c of this.components) {
if (c.endsWith('.css')) {
css += files.get(c) || '';
} else {
let v = files.get(c);
code = v + code;
}
}
}
code = code.replace(/import .*oda.js\'/g, '');
code = code.replace(/import .*(\'|\")\//g, `import $1${ODA.rootPath}/`);
code = code.replace(/(<oda-.*=\'|\")\//g, `$1${ODA.rootPath}/`);
this._iframe =
`
${css}
${this._scriptODA()}
<script type="module">
${code}
</script>
${this.filename.includes('.js') ? `<${this.filename.replace('.js', '')}></${this.filename.replace('.js', '')}>` : ''}
`;
} else if (this.filename && this.filename.includes('.html') || this._lang.includes('html')) {
code = code.replace(/\<script.*src="\//g, `<script type="module" src="${ODA.rootPath}/`);
code = code.replace(/\<script.*oda.js"\>\<\/script\>/g, this._scriptODA());
if (this.components && this.components.length) {
let v = '<script type="module">';
for (const c of this.components) {
if (c.endsWith('.css')) {
css += files.get(c) || '';
} else {
v += files.get(c) && files.get(c);
}
}
if (!code.includes('/oda.js"></script>')) code = `
${css}
${this._scriptODA()}` + code;
code += v + '</script>';
}
this._iframe = code;
}
this._iframe = this._iframe.replace(/alert\(/g, 'console.log("from alert > " + ');
this._iframe = `<meta charset="UTF-8">` + this._iframe;
this._setConsole();
this._src = this._iframe;
},
_getInfoIcon() {
return {
'error': 'icons:add-circle-outline:45', 'success': 'enterprise:check-box', 'info': 'icons:info-outline',
'warning': 'icons:warning', 'help': 'icons:help-outline', 'faq': 'icons:question-answer', 'like': 'shopping:like'
}[this.infpnl];
},
_getColor(left = false) {
let color = {
'error': 'red !important', 'success': 'green !important', 'info': 'blueviolet !important',
'warning': 'orange !important', 'help': 'blue !important', 'faq': 'brown !important', 'like': 'limegreen !important'
}[this.infpnl];
if (!color || !this.infpnl) return '';
if (!left)
return `fill:${color};border-color:${color}`;
return `border-left-color:${color};border-left-width:3px`;
},
_scriptODA() {
if (this._loadODA) return `<script type="module" src="${ODA.rootPath + '/oda.js'}"></script>`;
return `<script type="module" src="${ODA.rootPath + '/oda.js'}"></script>`;
}
})