`)
console.warn(err)
}
})
// image href new window(emoji not included)
const images = view.find('img.raw[src]').removeClass('raw')
images.each((key, value) => {
// if it's already wrapped by link, then ignore
const $value = $(value)
$value[0].onload = e => {
if (window.viewAjaxCallback) window.viewAjaxCallback()
}
})
// blockquote
const blockquote = view.find('blockquote.raw').removeClass('raw')
const blockquoteP = blockquote.find('p')
blockquoteP.each((key, value) => {
let html = $(value).html()
html = replaceExtraTags(html)
$(value).html(html)
})
// color tag in blockquote will change its left border color
const blockquoteColor = blockquote.find('.color')
blockquoteColor.each((key, value) => {
$(value).closest('blockquote').css('border-left-color', $(value).attr('data-color'))
})
// slideshare
view.find('div.slideshare.raw').removeClass('raw')
.each((key, value) => {
$.ajax({
type: 'GET',
url: `https://www.slideshare.net/api/oembed/2?url=https://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
jsonp: 'callback',
dataType: 'jsonp',
success (data) {
const $html = $(data.html)
const iframe = $html.closest('iframe')
const caption = $html.closest('div')
const inner = $('').append(iframe)
const height = iframe.attr('height')
const width = iframe.attr('width')
const ratio = (height / width) * 100
inner.css('padding-bottom', `${ratio}%`)
$(value).html(inner).append(caption)
if (window.viewAjaxCallback) window.viewAjaxCallback()
}
})
})
// speakerdeck
view.find('div.speakerdeck.raw').removeClass('raw')
.each((key, value) => {
const url = `https://speakerdeck.com/${$(value).attr('data-speakerdeckid')}`
const inner = $('Speakerdeck')
inner.attr('href', url)
inner.attr('rel', 'noopener noreferrer')
inner.attr('target', '_blank')
$(value).append(inner)
})
// pdf
view.find('div.pdf.raw').removeClass('raw')
.each(function (key, value) {
const url = $(value).attr('data-pdfurl')
const inner = $('')
$(this).append(inner)
PDFObject.embed(url, inner, {
height: '400px'
})
})
// syntax highlighting
view.find('code.raw').removeClass('raw')
.each((key, value) => {
const langDiv = $(value)
if (langDiv.length > 0) {
const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim()
if (reallang === 'mermaid' || reallang === 'abc' || reallang === 'graphviz') {
return
}
const codeDiv = langDiv.find('.code')
let code = ''
if (codeDiv.length > 0) code = codeDiv.html()
else code = langDiv.html()
let result
if (!reallang) {
result = {
value: code
}
} else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') {
code = S(code).unescapeHTML().s
result = {
value: Prism.highlight(code, Prism.languages[reallang])
}
} else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') {
code = S(code).unescapeHTML().s
result = {
value: Prism.highlight(code, Prism.languages.wiki)
}
} else if (reallang === 'cmake') {
code = S(code).unescapeHTML().s
result = {
value: Prism.highlight(code, Prism.languages.makefile)
}
} else {
require.ensure([], function (require) {
const hljs = require('highlight.js')
code = S(code).unescapeHTML().s
const languages = hljs.listLanguages()
if (!languages.includes(reallang)) {
result = hljs.highlightAuto(code)
} else {
result = hljs.highlight(reallang, code)
}
if (codeDiv.length > 0) codeDiv.html(result.value)
else langDiv.html(result.value)
})
return
}
if (codeDiv.length > 0) codeDiv.html(result.value)
else langDiv.html(result.value)
}
})
// mathjax
const mathjaxdivs = view.find('span.mathjax.raw').removeClass('raw').toArray()
try {
if (mathjaxdivs.length > 1) {
window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, mathjaxdivs])
window.MathJax.Hub.Queue(window.viewAjaxCallback)
} else if (mathjaxdivs.length > 0) {
window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, mathjaxdivs[0]])
window.MathJax.Hub.Queue(window.viewAjaxCallback)
}
} catch (err) {
console.warn(err)
}
// render title
document.title = renderTitle(view)
}
// only static transform should be here
export function postProcess (code) {
const result = $(`
${code}
`)
// process style tags
result.find('style').each((key, value) => {
let html = $(value).html()
// unescape > symbel inside the style tags
html = html.replace(/>/g, '>')
// remove css @import to prevent XSS
html = html.replace(/@import url\(([^)]*)\);?/gi, '')
$(value).html(html)
})
// link should open in new window or tab
// also add noopener to prevent clickjacking
// See details: https://mathiasbynens.github.io/rel-noopener/
result.find('a:not([href^="#"]):not([target])').attr('target', '_blank').attr('rel', 'noopener')
// If it's hashtag link then make it base uri independent
result.find('a[href^="#"]').each((index, linkTag) => {
const currentLocation = new URL(window.location)
currentLocation.hash = linkTag.hash
linkTag.href = currentLocation.toString()
})
// update continue line numbers
const linenumberdivs = result.find('.gutter.linenumber').toArray()
for (let i = 0; i < linenumberdivs.length; i++) {
if ($(linenumberdivs[i]).hasClass('continue')) {
const startnumber = linenumberdivs[i - 1] ? parseInt($(linenumberdivs[i - 1]).find('> span').last().attr('data-linenumber')) : 0
$(linenumberdivs[i]).find('> span').each((key, value) => {
$(value).attr('data-linenumber', startnumber + key + 1)
})
}
}
// show yaml meta paring error
if (md.metaError) {
let warning = result.find('div#meta-error')
if (warning && warning.length > 0) {
warning.text(md.metaError)
} else {
warning = $(`
${escapeHTML(md.metaError)}
`)
result.prepend(warning)
}
}
return result
}
window.postProcess = postProcess
const domevents = Object.getOwnPropertyNames(document).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(document)))).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(window))).filter(function (i) {
return !i.indexOf('on') && (document[i] === null || typeof document[i] === 'function')
}).filter(function (elem, pos, self) {
return self.indexOf(elem) === pos
})
export function removeDOMEvents (view) {
for (let i = 0, l = domevents.length; i < l; i++) {
view.find('[' + domevents[i] + ']').removeAttr(domevents[i])
}
}
window.removeDOMEvents = removeDOMEvents
function toDataURL (url, callback) {
fetch(url).then(response => {
const fr = new FileReader()
fr.onload = function () {
callback(this.result)
}
response.blob().then(blob => {
fr.readAsDataURL(blob)
})
})
}
function generateCleanHTML (view) {
const src = view.clone()
const eles = src.find('*')
// remove syncscroll parts
eles.removeClass('part')
src.find('*[class=""]').removeAttr('class')
eles.removeAttr('data-startline data-endline')
src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
// remove gist content
src.find('code[data-gist-id]').children().remove()
// disable todo list
src.find('input.task-list-item-checkbox').attr('disabled', '')
// replace emoji image path
src.find('img.emoji').each((key, value) => {
toDataURL($(value).attr('src'), dataURL => {
$(value).attr('src', dataURL)
})
})
// replace video to iframe
src.find('div[data-videoid]').each((key, value) => {
const id = $(value).attr('data-videoid')
const style = $(value).attr('style')
let url = null
if ($(value).hasClass('youtube')) {
url = 'https://www.youtube.com/embed/'
} else if ($(value).hasClass('vimeo')) {
url = 'https://player.vimeo.com/video/'
}
if (url) {
const iframe = $('')
iframe.attr('src', url + id)
iframe.attr('style', style)
$(value).html(iframe)
}
})
return src
}
export function exportToRawHTML (view) {
const filename = `${renderFilename(ui.area.markdown)}.html`
const src = generateCleanHTML(view)
$(src).find('a.anchor').remove()
const html = src[0].outerHTML
const blob = new Blob([html], {
type: 'text/html;charset=utf-8'
})
saveAs(blob, filename, true)
}
// extract markdown body to html and compile to template
export function exportToHTML (view) {
const title = renderTitle(ui.area.markdown)
const filename = `${renderFilename(ui.area.markdown)}.html`
const src = generateCleanHTML(view)
// generate toc
const toc = $('#ui-toc').clone()
toc.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
const tocAffix = $('#ui-toc-affix').clone()
tocAffix.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
// generate html via template
$.get(`${serverurl}/build/htmlexport.html`, template => {
let html = template.replace('{{{url}}}', serverurl)
html = html.replace('{{title}}', title)
html = html.replace('{{{html}}}', src[0].outerHTML)
html = html.replace('{{{ui-toc}}}', toc.html())
html = html.replace('{{{ui-toc-affix}}}', tocAffix.html())
html = html.replace('{{{lang}}}', (md && md.meta && md.meta.lang) ? `lang="${md.meta.lang}"` : '')
html = html.replace('{{{dir}}}', (md && md.meta && md.meta.dir) ? `dir="${md.meta.dir}"` : '')
const blob = new Blob([html], {
type: 'text/html;charset=utf-8'
})
saveAs(blob, filename, true)
})
}
// jQuery sortByDepth
$.fn.sortByDepth = function () {
const ar = this.map(function () {
return {
length: $(this).parents().length,
elt: this
}
}).get()
const result = []
let i = ar.length
ar.sort((a, b) => a.length - b.length)
while (i--) {
result.push(ar[i].elt)
}
return $(result)
}
function toggleTodoEvent (e) {
const startline = $(this).closest('li').attr('data-startline') - 1
const line = window.editor.getLine(startline)
const matches = line.match(/^[>\s-]*(?:[-+*]|\d+[.)])\s\[([xX ])]/)
if (matches && matches.length >= 2) {
let checked = null
if (matches[1].toLowerCase() === 'x') { checked = true } else if (matches[1] === ' ') { checked = false }
const replacements = matches[0].match(/(^[>\s-]*(?:[-+*]|\d+[.)])\s\[)([xX ])(])/)
window.editor.replaceRange(checked ? ' ' : 'x', {
line: startline,
ch: replacements[1].length
}, {
line: startline,
ch: replacements[1].length + 1
}, '+input')
}
}
// remove hash
function removeHash () {
history.pushState('', document.title, window.location.pathname + window.location.search)
}
let tocExpand = false
function checkExpandToggle () {
const toc = $('.ui-toc-dropdown .toc')
const toggle = $('.expand-toggle')
if (!tocExpand) {
toc.removeClass('expand')
toggle.text('Expand all')
} else {
toc.addClass('expand')
toggle.text('Collapse all')
}
}
// toc
export function generateToc (id) {
const target = $(`#${id}`)
target.html('')
/* eslint-disable no-unused-vars */
const toc = new window.Toc('doc', {
level: 3,
top: -1,
class: 'toc',
ulClass: 'nav',
targetId: id,
process: getHeaderContent
})
/* eslint-enable no-unused-vars */
if (target.text() === 'undefined') { target.html('') }
const tocMenu = $('Expand all')
const backtotop = $('Back to top')
const gotobottom = $('Go to bottom')
checkExpandToggle()
toggle.click(e => {
e.preventDefault()
e.stopPropagation()
tocExpand = !tocExpand
checkExpandToggle()
})
backtotop.click(e => {
e.preventDefault()
e.stopPropagation()
if (window.scrollToTop) { window.scrollToTop() }
removeHash()
})
gotobottom.click(e => {
e.preventDefault()
e.stopPropagation()
if (window.scrollToBottom) { window.scrollToBottom() }
removeHash()
})
tocMenu.append(toggle).append(backtotop).append(gotobottom)
target.append(tocMenu)
}
// smooth all hash trigger scrolling
export function smoothHashScroll () {
const hashElements = $("a[href^='#']:not([smoothhashscroll])").toArray()
for (const element of hashElements) {
const $element = $(element)
const hash = element.hash
if (hash) {
$element.on('click', function (e) {
// store hash
const hash = decodeURIComponent(this.hash)
// escape special characters in jquery selector
const $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, '\\$1'))
// return if no element been selected
if ($hash.length <= 0) return
// prevent default anchor click behavior
e.preventDefault()
// animate
$('body, html').stop(true, true).animate({
scrollTop: $hash.offset().top
}, 100, 'linear', () => {
// when done, add hash to url
// (default click behaviour)
window.location.hash = hash
})
})
$element.attr('smoothhashscroll', '')
}
}
}
function imgPlayiframe (element, src) {
if (!$(element).attr('data-videoid')) return
const iframe = $("")
$(iframe).attr('src', `${src + $(element).attr('data-videoid')}?autoplay=1`)
$(element).find('img').css('visibility', 'hidden')
$(element).append(iframe)
}
const anchorForId = id => {
const anchor = document.createElement('a')
anchor.ariaHidden = 'true'
anchor.className = 'anchor hidden-xs'
anchor.href = new URL(`#${id}`, document.location).toString()
anchor.innerHTML = ''
anchor.title = id
return anchor
}
const createHeaderId = (headerContent, headerIds = null) => {
// to escape characters not allow in css and humanize
const slug = slugifyWithUTF8(headerContent)
let id
if (window.linkifyHeaderStyle === 'keep-case') {
id = slug
} else if (window.linkifyHeaderStyle === 'lower-case') {
// to make compatible with GitHub, GitLab, Pandoc and many more
id = slug.toLowerCase()
} else if (window.linkifyHeaderStyle === 'gfm') {
// see GitHub implementation reference:
// https://gist.github.com/asabaylus/3071099#gistcomment-1593627
// it works like 'lower-case', but ...
const idBase = slug.toLowerCase()
id = idBase
if (headerIds !== null) {
// ... making sure the id is unique
let i = 1
while (headerIds.has(id)) {
id = idBase + '-' + i
i++
}
headerIds.add(id)
}
} else {
throw new Error('Unknown linkifyHeaderStyle value "' + window.linkifyHeaderStyle + '"')
}
return id
}
const linkifyAnchors = (level, containingElement) => {
const headers = containingElement.getElementsByTagName(`h${level}`)
for (let i = 0, l = headers.length; i < l; i++) {
const header = headers[i]
if (header.getElementsByClassName('anchor').length === 0) {
if (typeof header.id === 'undefined' || header.id === '') {
header.id = createHeaderId(getHeaderContent(header))
}
if (!(typeof header.id === 'undefined' || header.id === '')) {
header.insertBefore(anchorForId(header.id), header.firstChild)
}
}
}
}
export function autoLinkify (view) {
const contentBlock = view[0]
if (!contentBlock) {
return
}
for (let level = 1; level <= 6; level++) {
linkifyAnchors(level, contentBlock)
}
}
function getHeaderContent (header) {
const headerHTML = $(header).clone()
headerHTML.find('.MathJax_Preview').remove()
headerHTML.find('.MathJax').remove()
return headerHTML[0].innerHTML
}
function changeHeaderId ($header, id, newId) {
$header.attr('id', newId)
const $headerLink = $header.find(`> a.anchor[href="#${id}"]`)
$headerLink.attr('href', `#${newId}`)
$headerLink.attr('title', newId)
}
export function deduplicatedHeaderId (view) {
// headers contained in the last change
const headers = view.find(':header.raw').removeClass('raw').toArray()
if (headers.length === 0) {
return
}
if (window.linkifyHeaderStyle === 'gfm') {
// consistent with GitHub, GitLab, Pandoc & co.
// all headers contained in the document, in order of appearance
const allHeaders = view.find(':header').toArray()
// list of finaly assigned header IDs
const headerIds = new Set()
for (let j = 0; j < allHeaders.length; j++) {
const $header = $(allHeaders[j])
const id = $header.attr('id')
const newId = createHeaderId(getHeaderContent($header), headerIds)
changeHeaderId($header, id, newId)
}
} else {
// the legacy way
for (let i = 0; i < headers.length; i++) {
const id = $(headers[i]).attr('id')
if (!id) continue
const duplicatedHeaders = view.find(`:header[id="${id}"]`).toArray()
for (let j = 0; j < duplicatedHeaders.length; j++) {
if (duplicatedHeaders[j] !== headers[i]) {
const newId = id + j
const $header = $(duplicatedHeaders[j])
changeHeaderId($header, id, newId)
}
}
}
}
}
export function renderTOC (view) {
const tocs = view.find('.toc').toArray()
for (let i = 0; i < tocs.length; i++) {
const toc = $(tocs[i])
const id = `toc${i}`
toc.attr('id', id)
const target = $(`#${id}`)
target.html('')
/* eslint-disable no-unused-vars */
const TOC = new window.Toc('doc', {
level: 3,
top: -1,
class: 'toc',
targetId: id,
process: getHeaderContent
})
/* eslint-enable no-unused-vars */
if (target.text() === 'undefined') { target.html('') }
target.replaceWith(target.html())
}
}
export function scrollToHash () {
const hash = location.hash
location.hash = ''
location.hash = hash
}
function highlightRender (code, lang) {
if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return }
code = S(code).escapeHTML().s
if (lang === 'sequence') {
return `
${code}
`
} else if (lang === 'flow') {
return `
${code}
`
} else if (lang === 'graphviz') {
return `
${code}
`
} else if (lang === 'mermaid') {
return `
${code}
`
} else if (lang === 'abc') {
return `
${code}
`
}
const result = {
value: code
}
const showlinenumbers = /=$|=\d+$|=\+$/.test(lang)
if (showlinenumbers) {
let startnumber = 1
const matches = lang.match(/=(\d+)$/)
if (matches) { startnumber = parseInt(matches[1]) }
const lines = result.value.split('\n')
const linenumbers = []
for (let i = 0; i < lines.length - 1; i++) {
linenumbers[i] = ``
}
const continuelinenumber = /=\+$/.test(lang)
const linegutter = `