diff --git a/static/ansi-to-html.js b/static/ansi-to-html.js new file mode 100644 index 0000000..086f6ef --- /dev/null +++ b/static/ansi-to-html.js @@ -0,0 +1,547 @@ +'use strict'; + +// Original From https://github.com/rburns/ansi-to-html/blob/master/lib/ansi_to_html.js + +//const entities = require('entities'); +const defaults = { + fg: '#FFF', + bg: '#000', + newline: false, + stream: false, + colors: getDefaultColors() +}; + +function getDefaultColors() { + const colors = { + 0: '#AAA', // no, NOT BLACK + 1: '#A00', + 2: '#0A0', + 3: '#A50', + 4: '#0000d5', + 5: '#A0A', + 6: '#0AA', + 7: '#AAA', + 8: '#777', + 9: '#F55', + 10: '#5F5', + 11: '#FF5', + 12: '#55F', + 13: '#F5F', + 14: '#5FF', + 15: '#FFF' + }; + + range(0, 5).forEach(red => { + range(0, 5).forEach(green => { + range(0, 5).forEach(blue => setStyleColor(red, green, blue, colors)); + }); + }); + + range(0, 23).forEach(function (gray) { + const c = gray + 232; + const l = toHexString(gray * 10 + 8); + + colors[c] = '#' + l + l + l; + }); + + return colors; +} + +/** + * @param {number} red + * @param {number} green + * @param {number} blue + * @param {object} colors + */ +function setStyleColor(red, green, blue, colors) { + const c = 16 + (red * 36) + (green * 6) + blue; + const r = red > 0 ? red * 40 + 55 : 0; + const g = green > 0 ? green * 40 + 55 : 0; + const b = blue > 0 ? blue * 40 + 55 : 0; + + colors[c] = toColorHexString([r, g, b]); +} + +/** + * Converts from a number like 15 to a hex string like 'F' + * @param {number} num + * @returns {string} + */ +function toHexString(num) { + let str = num.toString(16); + + while (str.length < 2) { + str = '0' + str; + } + + return str; +} + +/** + * Converts from an array of numbers like [15, 15, 15] to a hex string like 'FFF' + * @returns {string} + * @param {number[]} ref + */ +function toColorHexString(ref) { + const results = []; + + for (const r of ref) { + results.push(toHexString(r)); + } + + return '#' + results.join(''); +} + +/** + * @param {Array} stack + * @param {string} token + * @param {*} data + * @param {object} options + */ +function generateOutput(stack, token, data, options) { + let result; + + if (token === 'text') { + result = pushText(data, options); + } else if (token === 'display') { + result = handleDisplay(stack, data, options); + } else if (token === 'xterm256Foreground') { + result = pushForegroundColor(stack, options.colors[data]); + } else if (token === 'xterm256Background') { + result = pushBackgroundColor(stack, options.colors[data]); + } else if (token === 'rgb') { + result = handleRgb(stack, data); + } + + return result; +} + +/** + * @param {Array} stack + * @param {string} data + * @returns {*} + */ +function handleRgb(stack, data) { + data = data.substring(2).slice(0, -1); + const operation = +data.substr(0, 2); + + const color = data.substring(5).split(';'); + const rgb = color.map(function (value) { + return ('0' + Number(value).toString(16)).substr(-2); + }).join(''); + + return pushStyle(stack, (operation === 38 ? 'color:#' : 'background-color:#') + rgb); +} + +/** + * @param {Array} stack + * @param {number} code + * @param {object} options + * @returns {*} + */ +function handleDisplay(stack, code, options) { + code = parseInt(code, 10); + + const codeMap = { + '-1': () => '
', + 0: () => stack.length && resetStyles(stack), + 1: () => pushTag(stack, 'b'), + 3: () => pushTag(stack, 'i'), + 4: () => pushTag(stack, 'u'), + 8: () => pushStyle(stack, 'display:none'), + 9: () => pushTag(stack, 'strike'), + 22: () => pushStyle(stack, 'font-weight:normal;text-decoration:none;font-style:normal'), + 23: () => closeTag(stack, 'i'), + 24: () => closeTag(stack, 'u'), + 39: () => pushForegroundColor(stack, options.fg), + 49: () => pushBackgroundColor(stack, options.bg), + 53: () => pushStyle(stack, 'text-decoration:overline') + }; + + let result; + if (codeMap[code]) { + result = codeMap[code](); + } else if (4 < code && code < 7) { + result = pushTag(stack, 'blink'); + } else if (29 < code && code < 38) { + result = pushForegroundColor(stack, options.colors[code - 30]); + } else if ((39 < code && code < 48)) { + result = pushBackgroundColor(stack, options.colors[code - 40]); + } else if ((89 < code && code < 98)) { + result = pushForegroundColor(stack, options.colors[8 + (code - 90)]); + } else if ((99 < code && code < 108)) { + result = pushBackgroundColor(stack, options.colors[8 + (code - 100)]); + } + + return result; +} + +/** + * Clear all the styles + * @returns {string} + */ +function resetStyles(stack) { + const stackClone = stack.slice(0); + + stack.length = 0; + + return stackClone.reverse().map(function (tag) { + return ''; + }).join(''); +} + +/** + * Creates an array of numbers ranging from low to high + * @param {number} low + * @param {number} high + * @returns {Array} + * @example range(3, 7); // creates [3, 4, 5, 6, 7] + */ +function range(low, high) { + const results = []; + + for (let j = low; j <= high; j++) { + results.push(j); + } + + return results; +} + + +/** + * Returns a new function that is true if value is NOT the same category + * @param {string} category + * @returns {function} + */ +function notCategory(category) { + return function (e) { + return (category === null || e.category !== category) && category !== 'all'; + }; +} + +/** + * Converts a code into an ansi token type + * @param {number} code + * @returns {string} + */ +function categoryForCode(code) { + code = parseInt(code, 10); + let result = null; + + if (code === 0) { + result = 'all'; + } else if (code === 1) { + result = 'bold'; + } else if ((2 < code && code < 5)) { + result = 'underline'; + } else if ((4 < code && code < 7)) { + result = 'blink'; + } else if (code === 8) { + result = 'hide'; + } else if (code === 9) { + result = 'strike'; + } else if ((29 < code && code < 38) || code === 39 || (89 < code && code < 98)) { + result = 'foreground-color'; + } else if ((39 < code && code < 48) || code === 49 || (99 < code && code < 108)) { + result = 'background-color'; + } + + return result; +} + +/** + * @param {string} text + * @param {object} options + * @returns {string} + */ +function pushText(text, options) { + return text; +} + +/** + * @param {Array} stack + * @param {string} tag + * @param {string} [style=''] + * @returns {string} + */ +function pushTag(stack, tag, style) { + if (!style) { + style = ''; + } + + stack.push(tag); + + return `<${tag}${style ? ` style="${style}"` : ''}>`; +} + +/** + * @param {Array} stack + * @param {string} style + * @returns {string} + */ +function pushStyle(stack, style) { + return pushTag(stack, 'span', style); +} + +function pushForegroundColor(stack, color) { + return pushTag(stack, 'span', 'color:' + color); +} + +function pushBackgroundColor(stack, color) { + return pushTag(stack, 'span', 'background-color:' + color); +} + +/** + * @param {Array} stack + * @param {string} style + * @returns {string} + */ +function closeTag(stack, style) { + let last; + + if (stack.slice(-1)[0] === style) { + last = stack.pop(); + } + + if (last) { + return ''; + } +} + +/** + * @param {string} text + * @param {object} options + * @param {function} callback + * @returns {Array} + */ +function tokenize(text, options, callback) { + let ansiMatch = false; + const ansiHandler = 3; + + function remove() { + return ''; + } + + function removeXterm256Foreground(m, g1) { + callback('xterm256Foreground', g1); + return ''; + } + + function removeXterm256Background(m, g1) { + callback('xterm256Background', g1); + return ''; + } + + function newline(m) { + if (options.newline) { + callback('display', -1); + } else { + callback('text', m); + } + + return ''; + } + + function ansiMess(m, g1) { + ansiMatch = true; + if (g1.trim().length === 0) { + g1 = '0'; + } + + g1 = g1.trimRight(';').split(';'); + + for (const g of g1) { + callback('display', g); + } + + return ''; + } + + function realText(m) { + callback('text', m); + + return ''; + } + + function rgb(m) { + callback('rgb', m); + + return ''; + } + + /* eslint no-control-regex:0 */ + const tokens = [{ + pattern: /^\x08+/, + sub: remove + }, { + pattern: /^\x1b\[[012]?K/, + sub: remove + }, { + pattern: /^\x1b\[\(B/, + sub: remove + }, { + pattern: /^\x1b\[[34]8;2;\d+;\d+;\d+m/, + sub: rgb + }, { + pattern: /^\x1b\[38;5;(\d+)m/, + sub: removeXterm256Foreground + }, { + pattern: /^\x1b\[48;5;(\d+)m/, + sub: removeXterm256Background + }, { + pattern: /^\n/, + sub: newline + }, { + pattern: /^\r+\n/, + sub: newline + }, { + pattern: /^\r/, + sub: newline + }, { + pattern: /^\x1b\[((?:\d{1,3};?)+|)m/, + sub: ansiMess + }, { + // CSI n J + // ED - Erase in Display Clears part of the screen. + // If n is 0 (or missing), clear from cursor to end of screen. + // If n is 1, clear from cursor to beginning of the screen. + // If n is 2, clear entire screen (and moves cursor to upper left on DOS ANSI.SYS). + // If n is 3, clear entire screen and delete all lines saved in the scrollback buffer + // (this feature was added for xterm and is supported by other terminal applications). + pattern: /^\x1b\[\d?J/, + sub: remove + }, { + // CSI n ; m f + // HVP - Horizontal Vertical Position Same as CUP + pattern: /^\x1b\[\d{0,3};\d{0,3}f/, + sub: remove + }, { + // catch-all for CSI sequences? + pattern: /^\x1b\[?[\d;]{0,3}/, + sub: remove + }, { + /** + * extracts real text - not containing: + * - `\x1b' - ESC - escape (Ascii 27) + * - '\x08' - BS - backspace (Ascii 8) + * - `\n` - Newline - linefeed (LF) (ascii 10) + * - `\r` - Windows Carriage Return (CR) + */ + pattern: /^(([^\x1b\x08\r\n])+)/, + sub: realText + }]; + + function process(handler, i) { + if (i > ansiHandler && ansiMatch) { + return; + } + + ansiMatch = false; + + text = text.replace(handler.pattern, handler.sub); + } + + const results1 = []; + let {length} = text; + + outer: + while (length > 0) { + for (let i = 0, o = 0, len = tokens.length; o < len; i = ++o) { + const handler = tokens[i]; + process(handler, i); + + if (text.length !== length) { + // We matched a token and removed it from the text. We need to + // start matching *all* tokens against the new text. + length = text.length; + continue outer; + } + } + + if (text.length === length) { + break; + } + results1.push(0); + + length = text.length; + } + + return results1; +} + +/** + * If streaming, then the stack is "sticky" + * + * @param {Array} stickyStack + * @param {string} token + * @param {*} data + * @returns {Array} + */ +function updateStickyStack(stickyStack, token, data) { + if (token !== 'text') { + stickyStack = stickyStack.filter(notCategory(categoryForCode(data))); + stickyStack.push({token, data, category: categoryForCode(data)}); + } + + return stickyStack; +} + +class Filter { + /** + * @param {object} options + * @param {string=} options.fg The default foreground color used when reset color codes are encountered. + * @param {string=} options.bg The default background color used when reset color codes are encountered. + * @param {boolean=} options.newline Convert newline characters to `
`. + * @param {boolean=} options.stream Save style state across invocations of `toHtml()`. + * @param {(string[] | {[code: number]: string})=} options.colors Can override specific colors or the entire ANSI palette. + */ + constructor(options) { + options = options || {}; + + if (options.colors) { + options.colors = Object.assign({}, defaults.colors, options.colors); + } + + this.options = Object.assign({}, defaults, options); + this.stack = []; + this.stickyStack = []; + } + + /** + * @param {string | string[]} input + * @returns {string} + */ + toHtml(input) { + input = typeof input === 'string' ? [input] : input; + const {stack, options} = this; + const buf = []; + + this.stickyStack.forEach(element => { + const output = generateOutput(stack, element.token, element.data, options); + + if (output) { + buf.push(output); + } + }); + + tokenize(input.join(''), options, (token, data) => { + const output = generateOutput(stack, token, data, options); + + if (output) { + buf.push(output); + } + + if (options.stream) { + this.stickyStack = updateStickyStack(this.stickyStack, token, data); + } + }); + + if (stack.length) { + buf.push(resetStyles(stack)); + } + + return buf.join(''); + } +} + +//module.exports = Filter; \ No newline at end of file diff --git a/static/application.css b/static/application.css index 412a503..1153b1d 100644 --- a/static/application.css +++ b/static/application.css @@ -1,9 +1,12 @@ body { - background: #0d1117; + background: #000; padding: 20px 50px; margin: 0; } - +body pre.hljs { + color: #c9d1d9; + background: #000; +} /* textarea */ textarea { diff --git a/static/application.js b/static/application.js index e14c239..8d2f692 100644 --- a/static/application.js +++ b/static/application.js @@ -1,7 +1,7 @@ /* global $, hljs, window, document */ +const armbianBuildPrelude = '# Armbian ANSI build logs'; ///// represents a single document - var haste_document = function () { this.locked = false; }; @@ -17,35 +17,54 @@ haste_document.prototype.htmlEscape = function (s) { // Get this document from the server and lock it here haste_document.prototype.load = function (key, callback, lang) { + console.log("Loading document key", key, "lang", lang); var _this = this; $.ajax('/documents/' + key, { - type: 'get', - dataType: 'json', - success: function (res) { + type: 'get', dataType: 'json', success: function (res) { + console.log("Loaded success document key", key, "lang", lang); _this.locked = true; _this.key = key; _this.data = res.data; + + let final_language; + let highlighted; try { var high; if (lang === 'txt') { + console.log("Highlighting as text"); high = {value: _this.htmlEscape(res.data)}; + final_language = high.language; + highlighted = high.value; } else if (lang) { + console.log("Highlighting as", lang); high = hljs.highlight(lang, res.data); + final_language = high.language; + highlighted = high.value; } else { - high = hljs.highlightAuto(res.data); + console.log("Highlighting auto (err.. wrong)"); + if (res.data.startsWith(armbianBuildPrelude)) { + console.log("Highlighting as Armbian build log"); + final_language = "Armbian ANSI build logs" + highlighted = new Filter().toHtml(res.data); + } else { + high = hljs.highlightAuto(res.data); + final_language = high.language; + highlighted = high.value; + } } } catch (err) { // failed highlight, fall back on auto + console.log("Highlighting auto (fallback)"); high = hljs.highlightAuto(res.data); + final_language = high.language; + highlighted = high.value; } + //console.log("Language FINAL", final_language, "value", highlighted); callback({ - value: high.value, - key: key, - language: high.language || lang, - lineCount: res.data.split('\n').length - }); - }, - error: function () { + value: highlighted, key: key, language: final_language || lang, lineCount: res.data.split('\n').length + } + ); + }, error: function () { callback(false); } }); @@ -59,22 +78,14 @@ haste_document.prototype.save = function (data, callback) { this.data = data; var _this = this; $.ajax('/documents', { - type: 'post', - data: data, - dataType: 'json', - contentType: 'text/plain; charset=utf-8', - success: function (res) { + type: 'post', data: data, dataType: 'json', contentType: 'text/plain; charset=utf-8', success: function (res) { _this.locked = true; _this.key = res.key; var high = hljs.highlightAuto(data); callback(null, { - value: high.value, - key: res.key, - language: high.language, - lineCount: data.split('\n').length + value: high.value, key: res.key, language: high.language, lineCount: data.split('\n').length }); - }, - error: function (res) { + }, error: function (res) { try { callback($.parseJSON(res.responseText)); } catch (e) { @@ -164,19 +175,49 @@ haste.prototype.newDocument = function (hideHistory) { // due to the behavior of lookupTypeByExtension and lookupExtensionByType // Note: optimized for lookupTypeByExtension haste.extensionMap = { - rb: 'ruby', py: 'python', pl: 'perl', php: 'php', scala: 'scala', go: 'go', - xml: 'xml', html: 'xml', htm: 'xml', css: 'css', js: 'javascript', vbs: 'vbscript', - lua: 'lua', pas: 'delphi', java: 'java', cpp: 'cpp', cc: 'cpp', m: 'objectivec', - vala: 'vala', sql: 'sql', sm: 'smalltalk', lisp: 'lisp', ini: 'ini', - diff: 'diff', bash: 'bash', sh: 'bash', tex: 'tex', erl: 'erlang', hs: 'haskell', - md: 'markdown', txt: '', coffee: 'coffee', swift: 'swift' + rb: 'ruby', + py: 'python', + pl: 'perl', + php: 'php', + scala: 'scala', + go: 'go', + xml: 'xml', + html: 'xml', + htm: 'xml', + css: 'css', + js: 'javascript', + vbs: 'vbscript', + lua: 'lua', + pas: 'delphi', + java: 'java', + cpp: 'cpp', + cc: 'cpp', + m: 'objectivec', + vala: 'vala', + sql: 'sql', + sm: 'smalltalk', + lisp: 'lisp', + ini: 'ini', + diff: 'diff', + bash: 'bash', + sh: 'bash', + tex: 'tex', + erl: 'erlang', + hs: 'haskell', + md: 'markdown', + txt: '', + coffee: 'coffee', + swift: 'swift' }; // Look up the extension preferred for a type // If not found, return the type itself - which we'll place as the extension haste.prototype.lookupExtensionByType = function (type) { for (let key in haste.extensionMap) { - if (haste.extensionMap[key] === type) return key; + if (haste.extensionMap[key] === type) { + console.log("Found extension", key, "for type", type); + return key; + } } return type; }; @@ -184,7 +225,9 @@ haste.prototype.lookupExtensionByType = function (type) { // Look up the type for a given extension // If not found, return the extension - which we'll attempt to use as the type haste.prototype.lookupTypeByExtension = function (ext) { - return haste.extensionMap[ext] || ext; + let result = haste.extensionMap[ext] || ext; + console.log("Found type", result, "for extension", ext); + return result; }; // Add line numbers to the document @@ -206,6 +249,7 @@ haste.prototype.removeLineNumbers = function () { haste.prototype.loadDocument = function (key) { // Split the key up var parts = key.split('.', 2); + console.log("Loading document", key, "with parts", parts); // Ask for what we want var _this = this; _this.doc = new haste_document(); @@ -256,65 +300,39 @@ haste.prototype.lockDocument = function () { haste.prototype.configureButtons = function () { var _this = this; - this.buttons = [ - { - $where: $('#box2 .save'), - label: 'Save', - shortcutDescription: 'control + s', - shortcut: function (evt) { - return evt.ctrlKey && (evt.keyCode === 83); - }, - action: function () { - if (_this.$textarea.val().replace(/^\s+|\s+$/g, '') !== '') { - _this.lockDocument(); - } - } - }, - { - $where: $('#box2 .new'), - label: 'New', - shortcut: function (evt) { - return evt.ctrlKey && evt.keyCode === 78; - }, - shortcutDescription: 'control + n', - action: function () { - _this.newDocument(!_this.doc.key); - } - }, - { - $where: $('#box2 .duplicate'), - label: 'Duplicate & Edit', - shortcut: function (evt) { - return _this.doc.locked && evt.ctrlKey && evt.keyCode === 68; - }, - shortcutDescription: 'control + d', - action: function () { - _this.duplicateDocument(); - } - }, - { - $where: $('#box2 .raw'), - label: 'Just Text', - shortcut: function (evt) { - return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82; - }, - shortcutDescription: 'control + shift + r', - action: function () { - window.location.href = '/raw/' + _this.doc.key; - } - }, - { - $where: $('#box2 .twitter'), - label: 'Twitter', - shortcut: function (evt) { - return _this.options.twitter && _this.doc.locked && evt.shiftKey && evt.ctrlKey && evt.keyCode == 84; - }, - shortcutDescription: 'control + shift + t', - action: function () { - window.open('https://twitter.com/share?url=' + encodeURI(window.location.href)); + this.buttons = [{ + $where: $('#box2 .save'), label: 'Save', shortcutDescription: 'control + s', shortcut: function (evt) { + return evt.ctrlKey && (evt.keyCode === 83); + }, action: function () { + if (_this.$textarea.val().replace(/^\s+|\s+$/g, '') !== '') { + _this.lockDocument(); } } - ]; + }, { + $where: $('#box2 .new'), label: 'New', shortcut: function (evt) { + return evt.ctrlKey && evt.keyCode === 78; + }, shortcutDescription: 'control + n', action: function () { + _this.newDocument(!_this.doc.key); + } + }, { + $where: $('#box2 .duplicate'), label: 'Duplicate & Edit', shortcut: function (evt) { + return _this.doc.locked && evt.ctrlKey && evt.keyCode === 68; + }, shortcutDescription: 'control + d', action: function () { + _this.duplicateDocument(); + } + }, { + $where: $('#box2 .raw'), label: 'Just Text', shortcut: function (evt) { + return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82; + }, shortcutDescription: 'control + shift + r', action: function () { + window.location.href = '/raw/' + _this.doc.key; + } + }, { + $where: $('#box2 .twitter'), label: 'Twitter', shortcut: function (evt) { + return _this.options.twitter && _this.doc.locked && evt.shiftKey && evt.ctrlKey && evt.keyCode == 84; + }, shortcutDescription: 'control + shift + t', action: function () { + window.open('https://twitter.com/share?url=' + encodeURI(window.location.href)); + } + }]; for (var i = 0; i < this.buttons.length; i++) { this.configureButton(this.buttons[i]); } @@ -378,8 +396,7 @@ $(function () { var startPos = this.selectionStart; var endPos = this.selectionEnd; var scrollTop = this.scrollTop; - this.value = this.value.substring(0, startPos) + myValue + - this.value.substring(endPos, this.value.length); + this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos, this.value.length); this.focus(); this.selectionStart = startPos + myValue.length; this.selectionEnd = startPos + myValue.length; diff --git a/static/index.html b/static/index.html index 31a59fc..7a10221 100644 --- a/static/index.html +++ b/static/index.html @@ -19,6 +19,7 @@ +