1201 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1201 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* eslint-env browser, jquery */
 | |
| /* global moment, serverurl */
 | |
| 
 | |
| require('prismjs/themes/prism.css')
 | |
| require('prismjs/components/prism-wiki')
 | |
| require('prismjs/components/prism-haskell')
 | |
| require('prismjs/components/prism-go')
 | |
| require('prismjs/components/prism-typescript')
 | |
| require('prismjs/components/prism-jsx')
 | |
| require('prismjs/components/prism-makefile')
 | |
| require('prismjs/components/prism-gherkin')
 | |
| 
 | |
| import Prism from 'prismjs'
 | |
| import hljs from 'highlight.js'
 | |
| import PDFObject from 'pdfobject'
 | |
| import S from 'string'
 | |
| import { saveAs } from 'file-saver'
 | |
| 
 | |
| require('./lib/common/login')
 | |
| require('../vendor/md-toc')
 | |
| var Viz = require('viz.js')
 | |
| 
 | |
| import getUIElements from './lib/editor/ui-elements'
 | |
| const ui = getUIElements()
 | |
| 
 | |
| // auto update last change
 | |
| window.createtime = null
 | |
| window.lastchangetime = null
 | |
| window.lastchangeui = {
 | |
|   status: $('.ui-status-lastchange'),
 | |
|   time: $('.ui-lastchange'),
 | |
|   user: $('.ui-lastchangeuser'),
 | |
|   nouser: $('.ui-no-lastchangeuser')
 | |
| }
 | |
| 
 | |
| const ownerui = $('.ui-owner')
 | |
| 
 | |
| export function updateLastChange () {
 | |
|   if (!window.lastchangeui) return
 | |
|   if (window.createtime) {
 | |
|     if (window.createtime && !window.lastchangetime) {
 | |
|       window.lastchangeui.status.text('created')
 | |
|     } else {
 | |
|       window.lastchangeui.status.text('changed')
 | |
|     }
 | |
|     const time = window.lastchangetime || window.createtime
 | |
|     window.lastchangeui.time.html(moment(time).fromNow())
 | |
|     window.lastchangeui.time.attr('title', moment(time).format('llll'))
 | |
|   }
 | |
| }
 | |
| setInterval(updateLastChange, 60000)
 | |
| 
 | |
| window.lastchangeuser = null
 | |
| window.lastchangeuserprofile = null
 | |
| 
 | |
| export function updateLastChangeUser () {
 | |
|   if (window.lastchangeui) {
 | |
|     if (window.lastchangeuser && window.lastchangeuserprofile) {
 | |
|       const icon = window.lastchangeui.user.children('i')
 | |
|       icon.attr('title', window.lastchangeuserprofile.name).tooltip('fixTitle')
 | |
|       if (window.lastchangeuserprofile.photo) { icon.attr('style', `background-image:url(${window.lastchangeuserprofile.photo})`) }
 | |
|       window.lastchangeui.user.show()
 | |
|       window.lastchangeui.nouser.hide()
 | |
|     } else {
 | |
|       window.lastchangeui.user.hide()
 | |
|       window.lastchangeui.nouser.show()
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| window.owner = null
 | |
| window.ownerprofile = null
 | |
| 
 | |
| export function updateOwner () {
 | |
|   if (ownerui) {
 | |
|     if (window.owner && window.ownerprofile && window.owner !== window.lastchangeuser) {
 | |
|       const icon = ownerui.children('i')
 | |
|       icon.attr('title', window.ownerprofile.name).tooltip('fixTitle')
 | |
|       const styleString = `background-image:url(${window.ownerprofile.photo})`
 | |
|       if (window.ownerprofile.photo && icon.attr('style') !== styleString) { icon.attr('style', styleString) }
 | |
|       ownerui.show()
 | |
|     } else {
 | |
|       ownerui.hide()
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| // get title
 | |
| function getTitle (view) {
 | |
|   let title = ''
 | |
|   if (md && md.meta && md.meta.title && (typeof md.meta.title === 'string' || typeof md.meta.title === 'number')) {
 | |
|     title = md.meta.title
 | |
|   } else {
 | |
|     const h1s = view.find('h1')
 | |
|     if (h1s.length > 0) {
 | |
|       title = h1s.first().text()
 | |
|     } else {
 | |
|       title = null
 | |
|     }
 | |
|   }
 | |
|   return title
 | |
| }
 | |
| 
 | |
| // render title
 | |
| export function renderTitle (view) {
 | |
|   let title = getTitle(view)
 | |
|   if (title) {
 | |
|     title += ' - HackMD'
 | |
|   } else {
 | |
|     title = 'HackMD - Collaborative markdown notes'
 | |
|   }
 | |
|   return title
 | |
| }
 | |
| 
 | |
| // render filename
 | |
| export function renderFilename (view) {
 | |
|   let filename = getTitle(view)
 | |
|   if (!filename) {
 | |
|     filename = 'Untitled'
 | |
|   }
 | |
|   return filename
 | |
| }
 | |
| 
 | |
| // render tags
 | |
| export function renderTags (view) {
 | |
|   const tags = []
 | |
|   const rawtags = []
 | |
|   if (md && md.meta && md.meta.tags && (typeof md.meta.tags === 'string' || typeof md.meta.tags === 'number')) {
 | |
|     const metaTags = (`${md.meta.tags}`).split(',')
 | |
|     for (let i = 0; i < metaTags.length; i++) {
 | |
|       const text = metaTags[i].trim()
 | |
|       if (text) rawtags.push(text)
 | |
|     }
 | |
|   } else {
 | |
|     view.find('h6').each((key, value) => {
 | |
|       if (/^tags/gmi.test($(value).text())) {
 | |
|         const codes = $(value).find('code')
 | |
|         for (let i = 0; i < codes.length; i++) {
 | |
|           const text = codes[i].innerHTML.trim()
 | |
|           if (text) rawtags.push(text)
 | |
|         }
 | |
|       }
 | |
|     })
 | |
|   }
 | |
|   for (let i = 0; i < rawtags.length; i++) {
 | |
|     let found = false
 | |
|     for (let j = 0; j < tags.length; j++) {
 | |
|       if (tags[j] === rawtags[i]) {
 | |
|         found = true
 | |
|         break
 | |
|       }
 | |
|     }
 | |
|     if (!found) { tags.push(rawtags[i]) }
 | |
|   }
 | |
|   return tags
 | |
| }
 | |
| 
 | |
| function slugifyWithUTF8 (text) {
 | |
|   // remove html tags and trim spaces
 | |
|   let newText = S(text).trim().stripTags().s
 | |
|   // replace all spaces in between to dashes
 | |
|   newText = newText.replace(/\s+/g, '-')
 | |
|   // slugify string to make it valid for attribute
 | |
|   newText = newText.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '')
 | |
|   return newText
 | |
| }
 | |
| 
 | |
| export function isValidURL (str) {
 | |
|   const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
 | |
|         '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
 | |
|         '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
 | |
|         '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
 | |
|         '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
 | |
|         '(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
 | |
|   if (!pattern.test(str)) {
 | |
|     return false
 | |
|   } else {
 | |
|     return true
 | |
|   }
 | |
| }
 | |
| 
 | |
| // parse meta
 | |
| export function parseMeta (md, edit, view, toc, tocAffix) {
 | |
|   let lang = null
 | |
|   let dir = null
 | |
|   let breaks = true
 | |
|   if (md && md.meta) {
 | |
|     const meta = md.meta
 | |
|     lang = meta.lang
 | |
|     dir = meta.dir
 | |
|     breaks = meta.breaks
 | |
|   }
 | |
|     // text language
 | |
|   if (lang && typeof lang === 'string') {
 | |
|     view.attr('lang', lang)
 | |
|     toc.attr('lang', lang)
 | |
|     tocAffix.attr('lang', lang)
 | |
|     if (edit) { edit.attr('lang', lang) }
 | |
|   } else {
 | |
|     view.removeAttr('lang')
 | |
|     toc.removeAttr('lang')
 | |
|     tocAffix.removeAttr('lang')
 | |
|     if (edit) { edit.removeAttr('lang', lang) }
 | |
|   }
 | |
|     // text direction
 | |
|   if (dir && typeof dir === 'string') {
 | |
|     view.attr('dir', dir)
 | |
|     toc.attr('dir', dir)
 | |
|     tocAffix.attr('dir', dir)
 | |
|   } else {
 | |
|     view.removeAttr('dir')
 | |
|     toc.removeAttr('dir')
 | |
|     tocAffix.removeAttr('dir')
 | |
|   }
 | |
|     // breaks
 | |
|   if (typeof breaks === 'boolean' && !breaks) {
 | |
|     md.options.breaks = false
 | |
|   } else {
 | |
|     md.options.breaks = true
 | |
|   }
 | |
| }
 | |
| 
 | |
| window.viewAjaxCallback = null
 | |
| 
 | |
| // regex for extra tags
 | |
| const spaceregex = /\s*/
 | |
| const notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/
 | |
| let coloregex = /\[color=([#|(|)|\s|,|\w]*?)\]/
 | |
| coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, 'g')
 | |
| let nameregex = /\[name=(.*?)\]/
 | |
| let timeregex = /\[time=([:|,|+|-|(|)|\s|\w]*?)\]/
 | |
| const nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, 'g')
 | |
| nameregex = new RegExp(nameregex.source + notinhtmltagregex.source, 'g')
 | |
| timeregex = new RegExp(timeregex.source + notinhtmltagregex.source, 'g')
 | |
| 
 | |
| function replaceExtraTags (html) {
 | |
|   html = html.replace(coloregex, '<span class="color" data-color="$1"></span>')
 | |
|   html = html.replace(nameandtimeregex, '<small><i class="fa fa-user"></i> $1 <i class="fa fa-clock-o"></i> $2</small>')
 | |
|   html = html.replace(nameregex, '<small><i class="fa fa-user"></i> $1</small>')
 | |
|   html = html.replace(timeregex, '<small><i class="fa fa-clock-o"></i> $1</small>')
 | |
|   return html
 | |
| }
 | |
| 
 | |
| if (typeof window.mermaid !== 'undefined' && window.mermaid) window.mermaid.startOnLoad = false
 | |
| 
 | |
| // dynamic event or object binding here
 | |
| export function finishView (view) {
 | |
|     // todo list
 | |
|   const lis = view.find('li.raw').removeClass('raw').sortByDepth().toArray()
 | |
| 
 | |
|   for (let li of lis) {
 | |
|     let html = $(li).clone()[0].innerHTML
 | |
|     const p = $(li).children('p')
 | |
|     if (p.length === 1) {
 | |
|       html = p.html()
 | |
|       li = p[0]
 | |
|     }
 | |
|     html = replaceExtraTags(html)
 | |
|     li.innerHTML = html
 | |
|     let disabled = 'disabled'
 | |
|     if (typeof editor !== 'undefined' && window.havePermission()) { disabled = '' }
 | |
|     if (/^\s*\[[x ]\]\s*/.test(html)) {
 | |
|       li.innerHTML = html.replace(/^\s*\[ \]\s*/, `<input type="checkbox" class="task-list-item-checkbox "${disabled}><label></label>`)
 | |
|                 .replace(/^\s*\[x\]\s*/, `<input type="checkbox" class="task-list-item-checkbox" checked ${disabled}><label></label>`)
 | |
|       if (li.tagName.toLowerCase() !== 'li') {
 | |
|         li.parentElement.setAttribute('class', 'task-list-item')
 | |
|       } else {
 | |
|         li.setAttribute('class', 'task-list-item')
 | |
|       }
 | |
|     }
 | |
|     if (typeof editor !== 'undefined' && window.havePermission()) { $(li).find('input').change(toggleTodoEvent) }
 | |
|         // color tag in list will convert it to tag icon with color
 | |
|     const tagColor = $(li).closest('ul').find('.color')
 | |
|     tagColor.each((key, value) => {
 | |
|       $(value).addClass('fa fa-tag').css('color', $(value).attr('data-color'))
 | |
|     })
 | |
|   }
 | |
| 
 | |
|     // youtube
 | |
|   view.find('div.youtube.raw').removeClass('raw')
 | |
|         .click(function () {
 | |
|           imgPlayiframe(this, '//www.youtube.com/embed/')
 | |
|         })
 | |
|     // vimeo
 | |
|   view.find('div.vimeo.raw').removeClass('raw')
 | |
|         .click(function () {
 | |
|           imgPlayiframe(this, '//player.vimeo.com/video/')
 | |
|         })
 | |
|         .each((key, value) => {
 | |
|           $.ajax({
 | |
|             type: 'GET',
 | |
|             url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`,
 | |
|             jsonp: 'callback',
 | |
|             dataType: 'jsonp',
 | |
|             success (data) {
 | |
|               const thumbnailSrc = data[0].thumbnail_large
 | |
|               const image = `<img src="${thumbnailSrc}" />`
 | |
|               $(value).prepend(image)
 | |
|               if (window.viewAjaxCallback) window.viewAjaxCallback()
 | |
|             }
 | |
|           })
 | |
|         })
 | |
|     // gist
 | |
|   view.find('code[data-gist-id]').each((key, value) => {
 | |
|     if ($(value).children().length === 0) { $(value).gist(window.viewAjaxCallback) }
 | |
|   })
 | |
|     // sequence diagram
 | |
|   const sequences = view.find('div.sequence-diagram.raw').removeClass('raw')
 | |
|   sequences.each((key, value) => {
 | |
|     try {
 | |
|       var $value = $(value)
 | |
|       const $ele = $(value).parent().parent()
 | |
| 
 | |
|       const sequence = $value
 | |
|       sequence.sequenceDiagram({
 | |
|         theme: 'simple'
 | |
|       })
 | |
| 
 | |
|       $ele.addClass('sequence-diagram')
 | |
|       $value.children().unwrap().unwrap()
 | |
|       const svg = $ele.find('> svg')
 | |
|       svg[0].setAttribute('viewBox', `0 0 ${svg.attr('width')} ${svg.attr('height')}`)
 | |
|       svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet')
 | |
|     } catch (err) {
 | |
|       $value.unwrap()
 | |
|       $value.parent().append('<div class="alert alert-warning">' + err + '</div>')
 | |
|       console.warn(err)
 | |
|     }
 | |
|   })
 | |
|     // flowchart
 | |
|   const flow = view.find('div.flow-chart.raw').removeClass('raw')
 | |
|   flow.each((key, value) => {
 | |
|     try {
 | |
|       var $value = $(value)
 | |
|       const $ele = $(value).parent().parent()
 | |
| 
 | |
|       const chart = window.flowchart.parse($value.text())
 | |
|       $value.html('')
 | |
|       chart.drawSVG(value, {
 | |
|         'line-width': 2,
 | |
|         'fill': 'none',
 | |
|         'font-size': '16px',
 | |
|         'font-family': "'Andale Mono', monospace"
 | |
|       })
 | |
| 
 | |
|       $ele.addClass('flow-chart')
 | |
|       $value.children().unwrap().unwrap()
 | |
|     } catch (err) {
 | |
|       $value.unwrap()
 | |
|       $value.parent().append('<div class="alert alert-warning">' + err + '</div>')
 | |
|       console.warn(err)
 | |
|     }
 | |
|   })
 | |
|     // graphviz
 | |
|   var graphvizs = view.find('div.graphviz.raw').removeClass('raw')
 | |
|   graphvizs.each(function (key, value) {
 | |
|     try {
 | |
|       var $value = $(value)
 | |
|       var $ele = $(value).parent().parent()
 | |
| 
 | |
|       var graphviz = Viz($value.text())
 | |
|       if (!graphviz) throw Error('viz.js output empty graph')
 | |
|       $value.html(graphviz)
 | |
| 
 | |
|       $ele.addClass('graphviz')
 | |
|       $value.children().unwrap().unwrap()
 | |
|     } catch (err) {
 | |
|       $value.unwrap()
 | |
|       $value.parent().append('<div class="alert alert-warning">' + err + '</div>')
 | |
|       console.warn(err)
 | |
|     }
 | |
|   })
 | |
|     // mermaid
 | |
|   const mermaids = view.find('div.mermaid.raw').removeClass('raw')
 | |
|   mermaids.each((key, value) => {
 | |
|     try {
 | |
|       var $value = $(value)
 | |
|       const $ele = $(value).closest('pre')
 | |
| 
 | |
|       window.mermaid.mermaidAPI.parse($value.text())
 | |
|       $ele.addClass('mermaid')
 | |
|       $ele.html($value.text())
 | |
|       window.mermaid.init(undefined, $ele)
 | |
|     } catch (err) {
 | |
|       var errormessage = err
 | |
|       if (err.str) {
 | |
|         errormessage = err.str
 | |
|       }
 | |
| 
 | |
|       $value.unwrap()
 | |
|       $value.parent().append('<div class="alert alert-warning">' + errormessage + '</div>')
 | |
|       console.warn(errormessage)
 | |
|     }
 | |
|   })
 | |
|   // abc.js
 | |
|   const abcs = view.find('div.abc.raw').removeClass('raw')
 | |
|   abcs.each((key, value) => {
 | |
|     try {
 | |
|       var $value = $(value)
 | |
|       var $ele = $(value).parent().parent()
 | |
| 
 | |
|       window.ABCJS.renderAbc(value, $value.text())
 | |
| 
 | |
|       $ele.addClass('abc')
 | |
|       $value.children().unwrap().unwrap()
 | |
|       const svg = $ele.find('> svg')
 | |
|       svg[0].setAttribute('viewBox', `0 0 ${svg.attr('width')} ${svg.attr('height')}`)
 | |
|       svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet')
 | |
|     } catch (err) {
 | |
|       $value.unwrap()
 | |
|       $value.parent().append('<div class="alert alert-warning">' + err + '</div>')
 | |
|       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: `//www.slideshare.net/api/oembed/2?url=http://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 = $('<div class="inner"></div>').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/oembed.json?url=https%3A%2F%2Fspeakerdeck.com%2F${encodeURIComponent($(value).attr('data-speakerdeckid'))}`
 | |
|             // use yql because speakerdeck not support jsonp
 | |
|           $.ajax({
 | |
|             url: 'https://query.yahooapis.com/v1/public/yql',
 | |
|             data: {
 | |
|               q: `select * from json where url ='${url}'`,
 | |
|               format: 'json'
 | |
|             },
 | |
|             dataType: 'jsonp',
 | |
|             success (data) {
 | |
|               if (!data.query || !data.query.results) return
 | |
|               const json = data.query.results.json
 | |
|               const html = json.html
 | |
|               var ratio = json.height / json.width
 | |
|               $(value).html(html)
 | |
|               const iframe = $(value).children('iframe')
 | |
|               const src = iframe.attr('src')
 | |
|               if (src.indexOf('//') === 0) { iframe.attr('src', `https:${src}`) }
 | |
|               const inner = $('<div class="inner"></div>').append(iframe)
 | |
|               const height = iframe.attr('height')
 | |
|               const width = iframe.attr('width')
 | |
|               ratio = (height / width) * 100
 | |
|               inner.css('padding-bottom', `${ratio}%`)
 | |
|               $(value).html(inner)
 | |
|               if (window.viewAjaxCallback) window.viewAjaxCallback()
 | |
|             }
 | |
|           })
 | |
|         })
 | |
|     // pdf
 | |
|   view.find('div.pdf.raw').removeClass('raw')
 | |
|             .each(function (key, value) {
 | |
|               const url = $(value).attr('data-pdfurl')
 | |
|               const inner = $('<div></div>')
 | |
|               $(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()
 | |
|             const codeDiv = langDiv.find('.code')
 | |
|             let code = ''
 | |
|             if (codeDiv.length > 0) code = codeDiv.html()
 | |
|             else code = langDiv.html()
 | |
|             var 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 {
 | |
|               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)
 | |
|           }
 | |
|         })
 | |
|     // 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 = $(`<div>${code}</div>`)
 | |
|   // 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
 | |
|   result.find('a:not([href^="#"]):not([target])').attr('target', '_blank')
 | |
|   // 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) {
 | |
|     var warning = result.find('div#meta-error')
 | |
|     if (warning && warning.length > 0) {
 | |
|       warning.text(md.metaError)
 | |
|     } else {
 | |
|       warning = $('<div id="meta-error" class="alert alert-warning">' + md.metaError + '</div>')
 | |
|       result.prepend(warning)
 | |
|     }
 | |
|   }
 | |
|   return result
 | |
| }
 | |
| window.postProcess = postProcess
 | |
| 
 | |
| var 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 (var i = 0, l = domevents.length; i < l; i++) {
 | |
|     view.find('[' + domevents[i] + ']').removeAttr(domevents[i])
 | |
|   }
 | |
| }
 | |
| window.removeDOMEvents = removeDOMEvents
 | |
| 
 | |
| 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) => {
 | |
|     let name = $(value).attr('alt')
 | |
|     name = name.substr(1)
 | |
|     name = name.slice(0, name.length - 1)
 | |
|     $(value).attr('src', `https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/images/basic/${name}.png`)
 | |
|   })
 | |
|     // 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 frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></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/html.min.css`, css => {
 | |
|     $.get(`${serverurl}/views/html.hbs`, data => {
 | |
|       const template = window.Handlebars.compile(data)
 | |
|       const context = {
 | |
|         url: serverurl,
 | |
|         title,
 | |
|         css,
 | |
|         html: src[0].outerHTML,
 | |
|         'ui-toc': toc.html(),
 | |
|         'ui-toc-affix': tocAffix.html(),
 | |
|         lang: (md && md.meta && md.meta.lang) ? `lang="${md.meta.lang}"` : null,
 | |
|         dir: (md && md.meta && md.meta.dir) ? `dir="${md.meta.dir}"` : null
 | |
|       }
 | |
|       const html = template(context)
 | |
|             //        console.log(html);
 | |
|       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-]*[-+*]\s\[([x ])\]/)
 | |
|   if (matches && matches.length >= 2) {
 | |
|     let checked = null
 | |
|     if (matches[1] === 'x') { checked = true } else if (matches[1] === ' ') { checked = false }
 | |
|     const replacements = matches[0].match(/(^[>\s-]*[-+*]\s\[)([x ])(\])/)
 | |
|     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 */
 | |
|   var 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 = $('<div class="toc-menu"></div')
 | |
|   const toggle = $('<a class="expand-toggle" href="#">Expand all</a>')
 | |
|   const backtotop = $('<a class="back-to-top" href="#">Back to top</a>')
 | |
|   const gotobottom = $('<a class="go-to-bottom" href="#">Go to bottom</a>')
 | |
|   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 frameborder='0' webkitallowfullscreen mozallowfullscreen allowfullscreen></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.className = 'anchor hidden-xs'
 | |
|   anchor.href = `#${id}`
 | |
|   anchor.innerHTML = '<span class="octicon octicon-link"></span>'
 | |
|   anchor.title = id
 | |
|   return anchor
 | |
| }
 | |
| 
 | |
| const linkifyAnchors = (level, containingElement) => {
 | |
|   const headers = containingElement.getElementsByTagName(`h${level}`)
 | |
| 
 | |
|   for (let i = 0, l = headers.length; i < l; i++) {
 | |
|     let header = headers[i]
 | |
|     if (header.getElementsByClassName('anchor').length === 0) {
 | |
|       if (typeof header.id === 'undefined' || header.id === '') {
 | |
|                 // to escape characters not allow in css and humanize
 | |
|         const id = slugifyWithUTF8(getHeaderContent(header))
 | |
|         header.id = 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
 | |
| }
 | |
| 
 | |
| export function deduplicatedHeaderId (view) {
 | |
|   const headers = view.find(':header.raw').removeClass('raw').toArray()
 | |
|   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 $duplicatedHeader = $(duplicatedHeaders[j])
 | |
|         $duplicatedHeader.attr('id', newId)
 | |
|         const $headerLink = $duplicatedHeader.find(`> a.anchor[href="#${id}"]`)
 | |
|         $headerLink.attr('href', `#${newId}`)
 | |
|         $headerLink.attr('title', 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 */
 | |
|     let 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 `<div class="sequence-diagram raw">${code}</div>`
 | |
|   } else if (lang === 'flow') {
 | |
|     return `<div class="flow-chart raw">${code}</div>`
 | |
|   } else if (lang === 'graphviz') {
 | |
|     return `<div class="graphviz raw">${code}</div>`
 | |
|   } else if (lang === 'mermaid') {
 | |
|     return `<div class="mermaid raw">${code}</div>`
 | |
|   } else if (lang === 'abc') {
 | |
|     return `<div class="abc raw">${code}</div>`
 | |
|   }
 | |
|   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] = `<span data-linenumber='${startnumber + i}'></span>`
 | |
|     }
 | |
|     const continuelinenumber = /=\+$/.test(lang)
 | |
|     const linegutter = `<div class='gutter linenumber${continuelinenumber ? ' continue' : ''}'>${linenumbers.join('\n')}</div>`
 | |
|     result.value = `<div class='wrapper'>${linegutter}<div class='code'>${result.value}</div></div>`
 | |
|   }
 | |
|   return result.value
 | |
| }
 | |
| 
 | |
| import markdownit from 'markdown-it'
 | |
| import markdownitContainer from 'markdown-it-container'
 | |
| 
 | |
| export let md = markdownit('default', {
 | |
|   html: true,
 | |
|   breaks: true,
 | |
|   langPrefix: '',
 | |
|   linkify: true,
 | |
|   typographer: true,
 | |
|   highlight: highlightRender
 | |
| })
 | |
| window.md = md
 | |
| 
 | |
| md.use(require('markdown-it-abbr'))
 | |
| md.use(require('markdown-it-footnote'))
 | |
| md.use(require('markdown-it-deflist'))
 | |
| md.use(require('markdown-it-mark'))
 | |
| md.use(require('markdown-it-ins'))
 | |
| md.use(require('markdown-it-sub'))
 | |
| md.use(require('markdown-it-sup'))
 | |
| md.use(require('markdown-it-mathjax')({
 | |
|   beforeMath: '<span class="mathjax raw">',
 | |
|   afterMath: '</span>',
 | |
|   beforeInlineMath: '<span class="mathjax raw">\\(',
 | |
|   afterInlineMath: '\\)</span>',
 | |
|   beforeDisplayMath: '<span class="mathjax raw">\\[',
 | |
|   afterDisplayMath: '\\]</span>'
 | |
| }))
 | |
| md.use(require('markdown-it-imsize'))
 | |
| 
 | |
| md.use(require('markdown-it-emoji'), {
 | |
|   shortcuts: {}
 | |
| })
 | |
| 
 | |
| window.emojify.setConfig({
 | |
|   blacklist: {
 | |
|     elements: ['script', 'textarea', 'a', 'pre', 'code', 'svg'],
 | |
|     classes: ['no-emojify']
 | |
|   },
 | |
|   img_dir: `${serverurl}/build/emojify.js/dist/images/basic`,
 | |
|   ignore_emoticons: true
 | |
| })
 | |
| 
 | |
| md.renderer.rules.emoji = (token, idx) => window.emojify.replace(`:${token[idx].markup}:`)
 | |
| 
 | |
| function renderContainer (tokens, idx, options, env, self) {
 | |
|   tokens[idx].attrJoin('role', 'alert')
 | |
|   tokens[idx].attrJoin('class', 'alert')
 | |
|   tokens[idx].attrJoin('class', `alert-${tokens[idx].info.trim()}`)
 | |
|   return self.renderToken(...arguments)
 | |
| }
 | |
| md.use(markdownitContainer, 'success', { render: renderContainer })
 | |
| md.use(markdownitContainer, 'info', { render: renderContainer })
 | |
| md.use(markdownitContainer, 'warning', { render: renderContainer })
 | |
| md.use(markdownitContainer, 'danger', { render: renderContainer })
 | |
| 
 | |
| let defaultImageRender = md.renderer.rules.image
 | |
| md.renderer.rules.image = function (tokens, idx, options, env, self) {
 | |
|   tokens[idx].attrJoin('class', 'raw')
 | |
|   return defaultImageRender(...arguments)
 | |
| }
 | |
| md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) {
 | |
|   tokens[idx].attrJoin('class', 'raw')
 | |
|   return self.renderToken(...arguments)
 | |
| }
 | |
| md.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) {
 | |
|   tokens[idx].attrJoin('class', 'raw')
 | |
|   return self.renderToken(...arguments)
 | |
| }
 | |
| md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
 | |
|   tokens[idx].attrJoin('class', 'raw')
 | |
|   return self.renderToken(...arguments)
 | |
| }
 | |
| md.renderer.rules.fence = (tokens, idx, options, env, self) => {
 | |
|   const token = tokens[idx]
 | |
|   const info = token.info ? md.utils.unescapeAll(token.info).trim() : ''
 | |
|   let langName = ''
 | |
|   let highlighted
 | |
| 
 | |
|   if (info) {
 | |
|     langName = info.split(/\s+/g)[0]
 | |
|     if (/!$/.test(info)) token.attrJoin('class', 'wrap')
 | |
|     token.attrJoin('class', options.langPrefix + langName.replace(/=$|=\d+$|=\+$|!$|=!$/, ''))
 | |
|     token.attrJoin('class', 'hljs')
 | |
|     token.attrJoin('class', 'raw')
 | |
|   }
 | |
| 
 | |
|   if (options.highlight) {
 | |
|     highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content)
 | |
|   } else {
 | |
|     highlighted = md.utils.escapeHtml(token.content)
 | |
|   }
 | |
| 
 | |
|   if (highlighted.indexOf('<pre') === 0) {
 | |
|     return `${highlighted}\n`
 | |
|   }
 | |
| 
 | |
|   return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`
 | |
| }
 | |
| 
 | |
| /* Defined regex markdown it plugins */
 | |
| import Plugin from 'markdown-it-regexp'
 | |
| 
 | |
| // youtube
 | |
| const youtubePlugin = new Plugin(
 | |
|     // regexp to match
 | |
|     /{%youtube\s*([\d\D]*?)\s*%}/,
 | |
| 
 | |
|     (match, utils) => {
 | |
|       const videoid = match[1]
 | |
|       if (!videoid) return
 | |
|       const div = $('<div class="youtube raw"></div>')
 | |
|       div.attr('data-videoid', videoid)
 | |
|       const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`
 | |
|       const image = `<img src="${thumbnailSrc}" />`
 | |
|       div.append(image)
 | |
|       const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>'
 | |
|       div.append(icon)
 | |
|       return div[0].outerHTML
 | |
|     }
 | |
| )
 | |
| // vimeo
 | |
| const vimeoPlugin = new Plugin(
 | |
|     // regexp to match
 | |
|     /{%vimeo\s*([\d\D]*?)\s*%}/,
 | |
| 
 | |
|     (match, utils) => {
 | |
|       const videoid = match[1]
 | |
|       if (!videoid) return
 | |
|       const div = $('<div class="vimeo raw"></div>')
 | |
|       div.attr('data-videoid', videoid)
 | |
|       const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>'
 | |
|       div.append(icon)
 | |
|       return div[0].outerHTML
 | |
|     }
 | |
| )
 | |
| // gist
 | |
| const gistPlugin = new Plugin(
 | |
|     // regexp to match
 | |
|     /{%gist\s*([\d\D]*?)\s*%}/,
 | |
| 
 | |
|     (match, utils) => {
 | |
|       const gistid = match[1]
 | |
|       const code = `<code data-gist-id="${gistid}"></code>`
 | |
|       return code
 | |
|     }
 | |
| )
 | |
| // TOC
 | |
| const tocPlugin = new Plugin(
 | |
|     // regexp to match
 | |
|     /^\[TOC\]$/i,
 | |
| 
 | |
|     (match, utils) => '<div class="toc"></div>'
 | |
| )
 | |
| // slideshare
 | |
| const slidesharePlugin = new Plugin(
 | |
|     // regexp to match
 | |
|     /{%slideshare\s*([\d\D]*?)\s*%}/,
 | |
| 
 | |
|     (match, utils) => {
 | |
|       const slideshareid = match[1]
 | |
|       const div = $('<div class="slideshare raw"></div>')
 | |
|       div.attr('data-slideshareid', slideshareid)
 | |
|       return div[0].outerHTML
 | |
|     }
 | |
| )
 | |
| // speakerdeck
 | |
| const speakerdeckPlugin = new Plugin(
 | |
|     // regexp to match
 | |
|     /{%speakerdeck\s*([\d\D]*?)\s*%}/,
 | |
| 
 | |
|     (match, utils) => {
 | |
|       const speakerdeckid = match[1]
 | |
|       const div = $('<div class="speakerdeck raw"></div>')
 | |
|       div.attr('data-speakerdeckid', speakerdeckid)
 | |
|       return div[0].outerHTML
 | |
|     }
 | |
| )
 | |
| // pdf
 | |
| const pdfPlugin = new Plugin(
 | |
|     // regexp to match
 | |
|     /{%pdf\s*([\d\D]*?)\s*%}/,
 | |
| 
 | |
|     (match, utils) => {
 | |
|       const pdfurl = match[1]
 | |
|       if (!isValidURL(pdfurl)) return match[0]
 | |
|       const div = $('<div class="pdf raw"></div>')
 | |
|       div.attr('data-pdfurl', pdfurl)
 | |
|       return div[0].outerHTML
 | |
|     }
 | |
| )
 | |
| 
 | |
| // yaml meta, from https://github.com/eugeneware/remarkable-meta
 | |
| function get (state, line) {
 | |
|   const pos = state.bMarks[line]
 | |
|   const max = state.eMarks[line]
 | |
|   return state.src.substr(pos, max - pos)
 | |
| }
 | |
| 
 | |
| function meta (state, start, end, silent) {
 | |
|   if (start !== 0 || state.blkIndent !== 0) return false
 | |
|   if (state.tShift[start] < 0) return false
 | |
|   if (!get(state, start).match(/^---$/)) return false
 | |
| 
 | |
|   const data = []
 | |
|   for (var line = start + 1; line < end; line++) {
 | |
|     const str = get(state, line)
 | |
|     if (str.match(/^(\.{3}|-{3})$/)) break
 | |
|     if (state.tShift[line] < 0) break
 | |
|     data.push(str)
 | |
|   }
 | |
| 
 | |
|   if (line >= end) return false
 | |
| 
 | |
|   try {
 | |
|     md.meta = window.jsyaml.safeLoad(data.join('\n')) || {}
 | |
|     delete md.metaError
 | |
|   } catch (err) {
 | |
|     md.metaError = err
 | |
|     console.warn(err)
 | |
|     return false
 | |
|   }
 | |
| 
 | |
|   state.line = line + 1
 | |
| 
 | |
|   return true
 | |
| }
 | |
| 
 | |
| function metaPlugin (md) {
 | |
|   md.meta = md.meta || {}
 | |
|   md.block.ruler.before('code', 'meta', meta, {
 | |
|     alt: []
 | |
|   })
 | |
| }
 | |
| 
 | |
| md.use(metaPlugin)
 | |
| md.use(youtubePlugin)
 | |
| md.use(vimeoPlugin)
 | |
| md.use(gistPlugin)
 | |
| md.use(tocPlugin)
 | |
| md.use(slidesharePlugin)
 | |
| md.use(speakerdeckPlugin)
 | |
| md.use(pdfPlugin)
 | |
| 
 | |
| export default {
 | |
|   md
 | |
| }
 |