mirror of https://github.com/MISP/MISP
849 lines
27 KiB
JavaScript
849 lines
27 KiB
JavaScript
'use strict';
|
|
|
|
var debounceDelay = 150, slowDebounceDelay = 3000;
|
|
var renderTimer, scrollTimer, attackMatrixTimer, eventgraphTimer;
|
|
var scrollMap;
|
|
var $splitContainer, $editorContainer, $rawContainer, $viewerContainer, $fullContainer, $resizableHandle, $autocompletionCB, $syncScrollCB, $autoRenderMarkdownCB, $topBar, $lastModifiedField, $markdownDropdownRulesMenu, $markdownDropdownGeneralMenu, $toggleFullScreenMode, $loadingBackdrop
|
|
var $editor, $viewer, $raw
|
|
var $saveMarkdownButton, $mardownViewerToolbar
|
|
var loadingSpanAnimation = '<span id="loadingSpan" class="fa fa-spin fa-spinner" style="margin-left: 5px;"></span>';
|
|
|
|
var contentChanged = false
|
|
var defaultMode = 'viewer'
|
|
var currentMode
|
|
var splitEdit = true
|
|
var noEditorScroll = false // Necessary as onscroll cannot be unbound from CM
|
|
$(document).ready(function() {
|
|
$splitContainer = $('.split-container')
|
|
$editorContainer = $('#editor-container')
|
|
$viewerContainer = $('#viewer-container')
|
|
$rawContainer = $('div.raw-container')
|
|
$fullContainer = $('.markdownEditor-full-container')
|
|
$resizableHandle = $('#resizable-handle')
|
|
$editor = $('#editor')
|
|
$viewer = $('#viewer')
|
|
$raw = $('#raw')
|
|
$mardownViewerToolbar = $('#mardown-viewer-toolbar')
|
|
$loadingBackdrop = $('#loadingBackdrop')
|
|
$saveMarkdownButton = $('#saveMarkdownButton')
|
|
$autocompletionCB = $('#autocompletionCB')
|
|
$syncScrollCB = $('#syncScrollCB')
|
|
$autoRenderMarkdownCB = $('#autoRenderMarkdownCB')
|
|
$toggleFullScreenMode = $('#toggleFullScreenMode')
|
|
$topBar = $('#top-bar')
|
|
$lastModifiedField = $('#lastModifiedField')
|
|
$markdownDropdownRulesMenu = $('#markdown-dropdown-rules-menu')
|
|
$markdownDropdownGeneralMenu = $('#markdownDropdownGeneralMenu')
|
|
|
|
initMarkdownIt()
|
|
if (canEdit) {
|
|
initCodeMirror()
|
|
toggleSaveButton(false)
|
|
}
|
|
var mode = defaultMode;
|
|
if (window.location.hash) {
|
|
// Switch to mode by using #[mode_name] in URL
|
|
var anchor = window.location.hash.substr(1);
|
|
if ((canEdit && (anchor === 'editor' || anchor === 'splitscreen')) || anchor === 'viewer' || anchor === 'raw') {
|
|
mode = anchor;
|
|
}
|
|
}
|
|
setMode(mode)
|
|
if (canEdit) {
|
|
setEditorData(originalRaw);
|
|
|
|
$editorContainer.resizable({
|
|
handles: {
|
|
e: $resizableHandle
|
|
},
|
|
grid: 50,
|
|
minWidth: 300,
|
|
maxWidth: window.innerWidth -220 - 300,
|
|
start: function( event, ui ) {
|
|
ui.helper.detach().appendTo('.markdownEditor-full-container')
|
|
$fullContainer.css('position', 'inherit') // Need as resizable helper is position absolute
|
|
},
|
|
stop: function() {
|
|
$fullContainer.css('position', 'relative')
|
|
cm.refresh()
|
|
scrollMap = null;
|
|
},
|
|
helper: 'ui-resizable-helper'
|
|
})
|
|
}
|
|
|
|
doRender()
|
|
|
|
if (typeof injectCustomRulesMenu === 'function') {
|
|
injectCustomRulesMenu()
|
|
}
|
|
|
|
reloadRuleEnabledUI()
|
|
|
|
if (canEdit) {
|
|
$editorContainer.on('touchstart mouseover', function () {
|
|
noEditorScroll = false
|
|
$viewerContainer.off('scroll');
|
|
cm.on('scroll', function(event) {
|
|
if (!noEditorScroll) {
|
|
doScroll(syncResultScroll)
|
|
}
|
|
});
|
|
});
|
|
|
|
$viewerContainer.on('touchstart mouseover', function () {
|
|
noEditorScroll = true
|
|
$viewerContainer.on('scroll', function() {
|
|
doScroll(syncSrcScroll)
|
|
});
|
|
});
|
|
|
|
if (typeof cmCustomSetup === 'function') {
|
|
cmCustomSetup()
|
|
}
|
|
if (typeof insertCustomToolbarButtons === 'function') {
|
|
insertCustomToolbarButtons()
|
|
}
|
|
|
|
refreshLastUpdatedField()
|
|
$(window).bind('beforeunload', function(e){
|
|
if (!contentChanged) {
|
|
return undefined;
|
|
}
|
|
(e || window.event).returnValue = confirmationMessageUnsavedChanges; //Gecko + IE
|
|
return confirmationMessageUnsavedChanges; //Gecko + Webkit, Safari, Chrome etc.
|
|
})
|
|
}
|
|
})
|
|
|
|
function initMarkdownIt() {
|
|
var mdOptions = {
|
|
highlight: function (str, lang) {
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
try {
|
|
return hljs.highlight(lang, str, true).value;
|
|
} catch (__) {}
|
|
}
|
|
return ''; // use external default escaping
|
|
}
|
|
}
|
|
md = window.markdownit('default', mdOptions);
|
|
md.disable([ 'link', 'image' ])
|
|
md.renderer.rules.table_open = function () {
|
|
return '<table class="table table-striped">\n';
|
|
};
|
|
md.renderer.rules.paragraph_open = injectLineNumbers;
|
|
md.renderer.rules.heading_open = injectLineNumbers;
|
|
if (typeof markdownItCustomPostInit === 'function') {
|
|
markdownItCustomPostInit()
|
|
}
|
|
// patch md.fence to support mermaid
|
|
md.mermaid = mermaid
|
|
const fenceBackup = md.renderer.rules.fence.bind(md.renderer.rules)
|
|
// https://github.com/tylingsoft/markdown-it-mermaid/blob/master/src/index.js
|
|
md.renderer.rules.fence = function (tokens, idx, options, env, slf) {
|
|
const token = tokens[idx]
|
|
const code = token.content.trim()
|
|
if (token.info === 'mermaid') {
|
|
return renderMermaid(code)
|
|
}
|
|
const firstLine = code.split(/\n/)[0].trim()
|
|
if (firstLine === 'gantt' || firstLine === 'sequenceDiagram' || firstLine.match(/^graph (?:TB|BT|RL|LR|TD);?$/)) {
|
|
return renderMermaid(code)
|
|
}
|
|
return fenceBackup(tokens, idx, options, env, slf)
|
|
}
|
|
var mermaidTheme = 'neutral'
|
|
mermaid.mermaidAPI.initialize({
|
|
startOnLoad: false,
|
|
theme: mermaidTheme,
|
|
})
|
|
}
|
|
|
|
function renderMermaid(code) {
|
|
try {
|
|
var result = mermaid.mermaidAPI.render('mermaid-graph', code)
|
|
return '<div class="mermaid">' + (result !== undefined ? result : '- error while parsing mermaid graph -') + '</div>'
|
|
} catch (err) {
|
|
return '<pre>' + 'mermaid error:\n' + err.message + '</pre>'
|
|
}
|
|
}
|
|
|
|
function initCodeMirror() {
|
|
var cmOptions = {
|
|
mode: 'markdown',
|
|
theme:'default',
|
|
lineNumbers: true,
|
|
indentUnit: 4,
|
|
showCursorWhenSelecting: true,
|
|
lineWrapping: true,
|
|
scrollbarStyle: 'overlay',
|
|
extraKeys: {
|
|
"Esc": function(cm) {
|
|
},
|
|
"Ctrl-Space": "autocomplete",
|
|
"Ctrl-B": function() { replacementAction('bold') },
|
|
"Ctrl-I": function() { replacementAction('italic') },
|
|
"Ctrl-H": function() { replacementAction('heading') },
|
|
"Ctrl-M": function() { replacementAction('element') },
|
|
},
|
|
hintOptions: {
|
|
completeSingle: false
|
|
},
|
|
}
|
|
if (typeof cmCustomHints === 'function') {
|
|
cmOptions['hintOptions']['hint'] = cmCustomHints
|
|
// cmOptions['hintOptions']['hint'] = function (cm, options) {
|
|
// var result = cmCustomHints(cm, options);
|
|
// if (result) {
|
|
// CodeMirror.on(result, 'shown', function() {})
|
|
// }
|
|
// return result;
|
|
// }
|
|
}
|
|
cm = CodeMirror.fromTextArea($editor[0], cmOptions);
|
|
cm.on('changes', function(cm, event) {
|
|
if (event[0].origin !== 'setValue') {
|
|
invalidateContentCache()
|
|
}
|
|
doRender();
|
|
})
|
|
cm.on("keyup", function (cm, event) {
|
|
if (!cm.state.completionActive && /*Enables keyboard navigation in autocomplete list*/
|
|
event.keyCode != 13 && /*Enter - do not open autocomplete list just after item has been selected in it*/
|
|
$autocompletionCB.prop('checked')) {
|
|
cm.showHint()
|
|
}
|
|
});
|
|
checkIfFullScreenEnabled()
|
|
}
|
|
|
|
function markdownItToggleRule(rulename, event) {
|
|
if (event !== undefined) {
|
|
event.stopPropagation()
|
|
}
|
|
var enabled
|
|
var CustomRuleRes, rule
|
|
if (typeof markdownItToggleCustomRule === 'function') {
|
|
CustomRuleRes = markdownItToggleCustomRule(rulename, event)
|
|
}
|
|
if (CustomRuleRes.found) {
|
|
enabled = CustomRuleRes.enabled
|
|
} else {
|
|
if (rulename == 'image') {
|
|
rule = getRuleStatus('inline', 'ruler', 'image')
|
|
if (rule !== false) {
|
|
enabled = rule.enabled
|
|
}
|
|
} else if (rulename == 'link') {
|
|
rule = getRuleStatus('inline', 'ruler', 'link')
|
|
if (rule !== false) {
|
|
enabled = rule.enabled
|
|
}
|
|
}
|
|
}
|
|
if (enabled !== undefined) {
|
|
if (enabled) {
|
|
md.disable([rulename])
|
|
} else {
|
|
md.enable([rulename])
|
|
}
|
|
}
|
|
doRender()
|
|
reloadRuleEnabledUI()
|
|
}
|
|
|
|
function reloadRuleEnabledUI() {
|
|
var rulesToUpdate = [
|
|
['inline', 'ruler', 'image'],
|
|
['inline', 'ruler', 'link'],
|
|
['inline', 'ruler', 'MISP_element_rule'],
|
|
]
|
|
rulesToUpdate.forEach(function(rulePath) {
|
|
var rule = getRuleStatus(rulePath[0], rulePath[1], rulePath[2])
|
|
if (rule.enabled) {
|
|
$('#markdownparsing-' + rule.name + '-parsing-enabled').show()
|
|
$('#markdownparsing-' + rule.name + '-parsing-disabled').hide()
|
|
} else {
|
|
$('#markdownparsing-' + rule.name + '-parsing-enabled').hide()
|
|
$('#markdownparsing-' + rule.name + '-parsing-disabled').show()
|
|
}
|
|
})
|
|
}
|
|
|
|
function toggleSaveButton(enabled) {
|
|
$saveMarkdownButton
|
|
.prop('disabled', !enabled)
|
|
}
|
|
|
|
function toggleLoadingInSaveButton(saving) {
|
|
toggleSaveButton(!saving)
|
|
if (saving) {
|
|
$saveMarkdownButton.append(loadingSpanAnimation);
|
|
toggleMarkdownEditorLoading(true, 'Saving report')
|
|
} else {
|
|
$saveMarkdownButton.find('#loadingSpan').remove();
|
|
toggleMarkdownEditorLoading(false)
|
|
}
|
|
}
|
|
|
|
function toggleMarkdownEditorLoading(loading, message) {
|
|
if (loading) {
|
|
$loadingBackdrop.show()
|
|
$loadingBackdrop.append(
|
|
$('<div/>').css({
|
|
'font-size': '20px',
|
|
'color': 'white'
|
|
}).append(
|
|
$(loadingSpanAnimation).css({
|
|
'margin-right': '0.5em'
|
|
}),
|
|
$('<span/>').text(message)
|
|
)
|
|
)
|
|
} else {
|
|
$loadingBackdrop.empty().hide()
|
|
}
|
|
}
|
|
|
|
function toggleFullscreenMode() {
|
|
var wholeContainer = $fullContainer[0]
|
|
if (!document.fullscreenElement) {
|
|
beforeFullscreen()
|
|
wholeContainer.requestFullscreen();
|
|
} else {
|
|
if (document.exitFullscreen) {
|
|
afterFullscreen()
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
}
|
|
|
|
function beforeFullscreen() {
|
|
$editorContainer.css({ // reset dimension if resizeable helper was used
|
|
height: 'inherit',
|
|
width: '50%'
|
|
})
|
|
$editorContainer.resizable( "option", { maxWidth: window.innerWidth - 300 } );
|
|
}
|
|
|
|
function afterFullscreen() {
|
|
$editorContainer.resizable( "option", { maxWidth: window.innerWidth -220 - 300 } );
|
|
}
|
|
|
|
function checkIfFullScreenEnabled() {
|
|
if (!document.fullscreenEnabled) {
|
|
$toggleFullScreenMode.hide()
|
|
}
|
|
}
|
|
|
|
function invalidateContentCache() {
|
|
contentChanged = true
|
|
toggleSaveButton(true)
|
|
$lastModifiedField.addClass('label-important').text(changeDetectedMessage)
|
|
}
|
|
|
|
function revalidateContentCache() {
|
|
contentChanged = false
|
|
toggleSaveButton(false)
|
|
$lastModifiedField.removeClass('label-important')
|
|
}
|
|
|
|
function refreshLastUpdatedField() {
|
|
$lastModifiedField.text(moment(parseInt(lastModified)).fromNow())
|
|
}
|
|
|
|
function sanitizeObject(obj) {
|
|
var newObj = {}
|
|
for (var key of Object.keys(obj)) {
|
|
var newVal = $('</p>').text(obj[key]).html()
|
|
newObj[key] = newVal
|
|
}
|
|
return newObj
|
|
}
|
|
|
|
function hideAll() {
|
|
$rawContainer.hide()
|
|
$editorContainer.hide()
|
|
$viewerContainer.hide()
|
|
$resizableHandle.hide()
|
|
}
|
|
|
|
function setMode(mode) {
|
|
currentMode = mode
|
|
$mardownViewerToolbar.find('button').removeClass('btn-inverse')
|
|
$mardownViewerToolbar.find('button[data-togglemode="' + mode + '"]').addClass('btn-inverse')
|
|
hideAll()
|
|
$editorContainer.css('width', '');
|
|
if (mode === 'raw') {
|
|
$rawContainer.show()
|
|
}
|
|
if (mode === 'splitscreen') {
|
|
$resizableHandle.show()
|
|
$splitContainer.addClass('split-actif')
|
|
} else {
|
|
$resizableHandle.hide()
|
|
$splitContainer.removeClass('split-actif')
|
|
}
|
|
if (mode === 'viewer' || mode === 'splitscreen') {
|
|
$viewerContainer.show()
|
|
}
|
|
if (mode === 'editor' || mode === 'splitscreen') {
|
|
$editorContainer.show({
|
|
duration: 0,
|
|
complete: function() {
|
|
cm.refresh()
|
|
// Make sure to build the scrollmap after the rendering
|
|
setTimeout(function() {
|
|
scrollMap = buildScrollMap()
|
|
}, 500);
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
function getEditorData() {
|
|
return cm !== undefined ? cm.getValue() : originalRaw
|
|
}
|
|
|
|
function setEditorData(data) {
|
|
cm.setValue(data)
|
|
}
|
|
|
|
function saveMarkdown(confirmSave, callback) {
|
|
confirmSave = confirmSave === undefined ? true : confirmSave
|
|
if (modelNameForSave === undefined || markdownModelFieldNameForSave === undefined) {
|
|
console.log('Model or field not defined. Save not possible')
|
|
return
|
|
}
|
|
if (confirmSave && !confirm(saveConfirmMessage)) {
|
|
return
|
|
}
|
|
var url = baseurl + "/eventReports/edit/" + reportid
|
|
fetchFormDataAjax(url, function(formHTML) {
|
|
$('body').append($('<div id="temp" style="display: none"/>').html(formHTML))
|
|
var $tmpForm = $('#temp form')
|
|
var formUrl = $tmpForm.attr('action')
|
|
$tmpForm.find('[name="data[' + modelNameForSave + '][' + markdownModelFieldNameForSave + ']"]').val(getEditorData())
|
|
|
|
$.ajax({
|
|
data: $tmpForm.serialize(),
|
|
beforeSend: function() {
|
|
toggleLoadingInSaveButton(true)
|
|
$editor.prop('disabled', true);
|
|
},
|
|
success:function(report, textStatus) {
|
|
if (report) {
|
|
showMessage('success', report.message);
|
|
if (report.data !== undefined) {
|
|
lastModified = report.data.EventReport.timestamp + '000'
|
|
refreshLastUpdatedField()
|
|
originalRaw = report.data.EventReport.content
|
|
revalidateContentCache()
|
|
}
|
|
}
|
|
},
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
showMessage('fail', saveFailedMessage + ': ' + errorThrown);
|
|
},
|
|
complete:function() {
|
|
$('#temp').remove();
|
|
toggleLoadingInSaveButton(false)
|
|
$editor.prop('disabled', false);
|
|
if (callback !== undefined) {
|
|
callback()
|
|
}
|
|
},
|
|
type:"post",
|
|
url: formUrl
|
|
})
|
|
})
|
|
}
|
|
|
|
function cancelEdit() {
|
|
setEditorData(originalRaw);
|
|
setMode('viewer')
|
|
}
|
|
|
|
function downloadMarkdown(type) {
|
|
var content, fileType, baseName, extension
|
|
if (type == 'pdf') {
|
|
if (currentMode != 'viewer' && currentMode != 'splitscreen') {
|
|
setMode('viewer')
|
|
setTimeout(function (){ // let the parser render the document
|
|
if (confirm(savePDFConfirmMessage)) {
|
|
window.print()
|
|
}
|
|
}, 300);
|
|
} else {
|
|
if (confirm(savePDFConfirmMessage)) {
|
|
window.print()
|
|
}
|
|
}
|
|
return
|
|
} else if (type == 'text') {
|
|
content = getEditorData()
|
|
baseName = 'event-report-' + (new Date()).getTime()
|
|
extension = 'md'
|
|
fileType = 'text/markdown'
|
|
} else if (type == 'text-gfm') {
|
|
content = getEditorData()
|
|
if (typeof markdownGFMSubstitution === 'function') {
|
|
content = markdownGFMSubstitution(content)
|
|
}
|
|
baseName = 'event-report-' + (new Date()).getTime()
|
|
extension = 'md'
|
|
fileType = 'text/markdown'
|
|
}
|
|
var filename = baseName + '.' + extension
|
|
var blob = new Blob([content], {
|
|
type: fileType
|
|
})
|
|
saveAs(blob, filename)
|
|
}
|
|
|
|
function showHelp() {
|
|
$('#genericModal.markdown-modal-helper').modal();
|
|
}
|
|
|
|
function renderMarkdown() {
|
|
var toRender = getEditorData()
|
|
var result = md.render(toRender)
|
|
scrollMap = null
|
|
$viewer.html(result)
|
|
postRenderingAction()
|
|
}
|
|
|
|
function doRender() {
|
|
if ($autoRenderMarkdownCB.prop('checked')) {
|
|
clearTimeout(renderTimer);
|
|
renderTimer = setTimeout(renderMarkdown, debounceDelay);
|
|
}
|
|
}
|
|
|
|
function registerListener() {
|
|
if (typeof markdownCustomPostRenderingListener === 'function') {
|
|
markdownCustomPostRenderingListener()
|
|
}
|
|
}
|
|
|
|
function postRenderingAction() {
|
|
registerListener()
|
|
if (typeof markdownCustomPostRenderingActions === 'function') {
|
|
markdownCustomPostRenderingActions()
|
|
}
|
|
}
|
|
|
|
function replacementAction(action) {
|
|
var customReplacementTriggered = false
|
|
if (typeof customReplacementActions === 'function') {
|
|
customReplacementTriggered = customReplacementActions(action)
|
|
}
|
|
if (!customReplacementTriggered) {
|
|
baseReplacementAction(action)
|
|
}
|
|
}
|
|
|
|
function baseReplacementAction(action) {
|
|
var start = cm.getCursor('start')
|
|
var end = cm.getCursor('end')
|
|
var content = cm.getRange(start, end)
|
|
var replacement = content
|
|
var setCursorTo = false
|
|
|
|
switch (action) {
|
|
case 'bold':
|
|
replacement = '**' + content + '**'
|
|
break;
|
|
case 'italic':
|
|
replacement = '*' + content + '*'
|
|
break;
|
|
case 'heading':
|
|
start.ch = 0
|
|
replacement = cm.getRange({line: start.line, ch: 0}, {line: start.line, ch: 1}) == '#' ? '#' : '# '
|
|
end = null
|
|
break;
|
|
case 'strikethrough':
|
|
replacement = '~~' + content + '~~'
|
|
break;
|
|
case 'list-ul':
|
|
start.ch = 0
|
|
var currentFirstChar = cm.getRange({line: start.line, ch: 0}, {line: start.line, ch: 2})
|
|
if (currentFirstChar == '* ') {
|
|
replacement = ''
|
|
end.ch = 2
|
|
} else {
|
|
replacement = '* '
|
|
end = null
|
|
}
|
|
break;
|
|
case 'list-ol':
|
|
start.ch = 0
|
|
var currentFirstChar = cm.getRange({line: start.line, ch: 0}, {line: start.line, ch: 3})
|
|
if (currentFirstChar == '1. ') {
|
|
replacement = ''
|
|
end.ch = 3
|
|
} else {
|
|
replacement = '1. '
|
|
end = null
|
|
}
|
|
break;
|
|
case 'quote':
|
|
start.ch = 0
|
|
var currentFirstChar = cm.getRange({line: start.line, ch: 0}, {line: start.line, ch: 2})
|
|
if (currentFirstChar == '> ') {
|
|
replacement = ''
|
|
end.ch = 2
|
|
} else {
|
|
replacement = '> '
|
|
end = null
|
|
}
|
|
break;
|
|
case 'code':
|
|
cm.replaceRange('\n```', {line: start.line - 1})
|
|
cm.replaceRange('\n```', {line: end.line + 1})
|
|
cm.setCursor(start.line + 1)
|
|
cm.focus()
|
|
return;
|
|
case 'table':
|
|
var tableTemplate = '| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n'
|
|
var lineContent = cm.getLine(start.line)
|
|
if (lineContent != '') {
|
|
tableTemplate = '\n' + tableTemplate
|
|
}
|
|
cm.replaceRange(tableTemplate, {line: start.line + 1})
|
|
var startSelection = start.line + 1
|
|
if (lineContent != '') {
|
|
startSelection++
|
|
}
|
|
cm.setSelection({line: startSelection, ch: 2}, {line: startSelection, ch: 10})
|
|
cm.focus()
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
cm.replaceRange(replacement, start, end)
|
|
if (setCursorTo !== false) {
|
|
cm.setCursor(setCursorTo.line, setCursorTo.ch)
|
|
}
|
|
cm.focus()
|
|
}
|
|
|
|
function setCMReadOnly(readonly) {
|
|
cm.setOption('readOnly', readonly)
|
|
}
|
|
|
|
function insertTopToolbarSection() {
|
|
$topBar.append($('<i />').addClass('top-bar-separator'))
|
|
}
|
|
|
|
function insertTopToolbarButton(FAClass, replacement, title) {
|
|
title = title === undefined ? '' : title
|
|
$topBar.append(
|
|
$('<span />').addClass('useCursorPointer icon fa fa-' + FAClass)
|
|
.attr('title', title)
|
|
.click(function() {
|
|
replacementAction(replacement)
|
|
})
|
|
)
|
|
}
|
|
|
|
// function createRulesMenuItem(ruleName, text, icon) {
|
|
function createRulesMenuItem(itemName, icon, ruleScope, ruleName) {
|
|
var functionName = 'markdownItToggleRule'
|
|
var classPrefix = 'parsing'
|
|
if (ruleScope == 'render' && typeof markdownItToggleRenderingRule === 'function') {
|
|
functionName = 'markdownItToggleRenderingRule'
|
|
classPrefix = 'rendering'
|
|
}
|
|
var $MISPElementMenuItem = $markdownDropdownRulesMenu.find('li:first').clone()
|
|
$MISPElementMenuItem.find('a').attr('onclick', functionName + '(\'' + ruleName + '\', arguments[0]); return false;')
|
|
if (icon instanceof jQuery){
|
|
$MISPElementMenuItem.find('span.icon').empty().append(icon)
|
|
} else {
|
|
$MISPElementMenuItem.find('span.icon > i').attr('class', 'fas fa-'+icon)
|
|
}
|
|
$MISPElementMenuItem.find('span.ruleText').text(itemName)
|
|
$MISPElementMenuItem.find('span.bold.green').attr('id', 'markdown' + classPrefix + '-' + ruleName + '-' + classPrefix + '-enabled')
|
|
$MISPElementMenuItem.find('span.bold.red').attr('id', 'markdown' + classPrefix + '-' + ruleName + '-' + classPrefix + '-disabled')
|
|
return $MISPElementMenuItem
|
|
}
|
|
|
|
function createMenuItem(itemName, icon, clickHandler) {
|
|
return $('<li/>').append(
|
|
$('<a/>').attr('tabindex', '-1').attr('href', '#').click(function (event) {
|
|
event.preventDefault();
|
|
clickHandler();
|
|
}).append(
|
|
$('<span/>').addClass('icon').append(
|
|
icon instanceof jQuery ? icon : $('<i/>').addClass(icon)
|
|
),
|
|
$('<span/>').text(' ' + itemName)
|
|
)
|
|
)
|
|
}
|
|
|
|
function createSubMenu(submenuConfig) {
|
|
var $submenus = $('<ul/>').addClass('dropdown-menu')
|
|
submenuConfig.items.forEach(function(item) {
|
|
if (item.isToggleableRule) {
|
|
$submenus.append(createRulesMenuItem(item.name, item.icon, item.ruleScope, item.ruleName))
|
|
} else {
|
|
$submenus.append(createMenuItem(item.name, item.icon, item.clickHandler))
|
|
}
|
|
});
|
|
var $dropdownSubmenu = createMenuItem(submenuConfig.name, submenuConfig.icon).addClass('dropdown-submenu').append($submenus)
|
|
$markdownDropdownGeneralMenu.append($dropdownSubmenu)
|
|
}
|
|
|
|
function getRuleStatus(context, rulername, rulename) {
|
|
var rules = md[context][rulername].__rules__
|
|
for (var i = 0; i < rules.length; i++) {
|
|
var rule = rules[i];
|
|
if (rule.name == rulename) {
|
|
return rule
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
function isInsideModal() {
|
|
return $(".markdownEditor-full-container").closest('.modal').length > 0
|
|
}
|
|
|
|
// Inject line numbers for sync scroll. Notes:
|
|
//
|
|
// - We track only headings and paragraphs on first level. That's enough.
|
|
// - Footnotes content causes jumps. Level limit filter it automatically.
|
|
function injectLineNumbers(tokens, idx, options, env, slf) {
|
|
var line;
|
|
if (tokens[idx].map && tokens[idx].level === 0) {
|
|
line = tokens[idx].map[0];
|
|
tokens[idx].attrJoin('class', 'line');
|
|
tokens[idx].attrSet('data-line', String(line+1));
|
|
}
|
|
return slf.renderToken(tokens, idx, options, env, slf);
|
|
}
|
|
|
|
|
|
// Build offsets for each line (lines can be wrapped)
|
|
// That's a bit dirty to process each line everytime, but ok for demo.
|
|
// Optimizations are required only for big texts.
|
|
// Source: https://github.com/markdown-it/markdown-it/blob/master/support/demo_template/index.js
|
|
function buildScrollMap() {
|
|
var i, offset, nonEmptyList, pos, a, b, lineHeightMap, linesCount,
|
|
acc, sourceLikeDiv, textarea = $(cm.getWrapperElement()),
|
|
_scrollMap;
|
|
|
|
sourceLikeDiv = $('<div />').css({
|
|
position: 'absolute',
|
|
visibility: 'hidden',
|
|
height: 'auto',
|
|
width: textarea[0].clientWidth,
|
|
'font-size': textarea.css('font-size'),
|
|
'font-family': textarea.css('font-family'),
|
|
'line-height': textarea.css('line-height'),
|
|
'white-space': textarea.css('white-space')
|
|
}).appendTo('body');
|
|
|
|
offset = $viewerContainer.scrollTop() - $viewerContainer.offset().top;
|
|
if (isInsideModal()) { // inside a modal
|
|
offset -= 20
|
|
}
|
|
_scrollMap = [];
|
|
nonEmptyList = [];
|
|
lineHeightMap = [];
|
|
|
|
acc = 0;
|
|
cm.eachLine(function(line) {
|
|
var h, lh;
|
|
lineHeightMap.push(acc)
|
|
if (line.text.length === 0) {
|
|
acc++
|
|
return
|
|
}
|
|
sourceLikeDiv.text(line.text);
|
|
h = parseFloat(sourceLikeDiv.css('height'));
|
|
lh = parseFloat(sourceLikeDiv.css('line-height'));
|
|
acc += Math.round(h / lh);
|
|
})
|
|
sourceLikeDiv.remove();
|
|
lineHeightMap.push(acc);
|
|
linesCount = acc;
|
|
|
|
for (i = 0; i < linesCount; i++) { _scrollMap.push(-1); }
|
|
|
|
nonEmptyList.push(0);
|
|
_scrollMap[0] = 0;
|
|
|
|
$viewerContainer.find('.line').each(function (n, el) {
|
|
var $el = $(el), t = $el.data('line');
|
|
if (t === '') { return; }
|
|
t = lineHeightMap[t];
|
|
if (t !== 0) { nonEmptyList.push(t); }
|
|
_scrollMap[t] = Math.round($el.offset().top + offset);
|
|
});
|
|
|
|
nonEmptyList.push(linesCount);
|
|
_scrollMap[linesCount] = $viewerContainer[0].scrollHeight;
|
|
|
|
pos = 0;
|
|
for (i = 1; i < linesCount; i++) {
|
|
if (_scrollMap[i] !== -1) {
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
a = nonEmptyList[pos];
|
|
b = nonEmptyList[pos + 1];
|
|
_scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a));
|
|
}
|
|
|
|
return _scrollMap;
|
|
}
|
|
|
|
function doScroll(fun) {
|
|
if ($syncScrollCB.prop('checked')) {
|
|
clearTimeout(scrollTimer);
|
|
scrollTimer = setTimeout(fun, debounceDelay);
|
|
}
|
|
}
|
|
|
|
// Synchronize scroll position from source to result
|
|
var syncResultScroll = function () {
|
|
var lineNo = Math.ceil(cm.getScrollInfo().top/cm.defaultTextHeight());
|
|
if (!scrollMap) { scrollMap = buildScrollMap(); }
|
|
var posTo = scrollMap[lineNo];
|
|
$viewerContainer.stop(true).animate({
|
|
scrollTop: posTo
|
|
}, 100, 'linear');
|
|
}
|
|
|
|
// Synchronize scroll position from result to source
|
|
var syncSrcScroll = function () {
|
|
var resultHtml = $viewerContainer,
|
|
scrollTop = resultHtml.scrollTop(),
|
|
lines,
|
|
i,
|
|
line;
|
|
|
|
if (!scrollMap) { scrollMap = buildScrollMap(); }
|
|
|
|
lines = Object.keys(scrollMap);
|
|
|
|
if (lines.length < 1) {
|
|
return;
|
|
}
|
|
|
|
line = lines[0];
|
|
|
|
for (i = 1; i < lines.length; i++) {
|
|
if (scrollMap[lines[i]] < scrollTop) {
|
|
line = lines[i];
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
cm.scrollTo(0, line*cm.defaultTextHeight())
|
|
}
|