/* This file is part of human-resource-machine-viewer, copyright 2015 Alan De Smet. human-resource-machine-viewer is free software you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. human-resource-machine-viewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with human-resource-machine-view. If not, see . */ //////////////////////////////////////////////////////////////////////////////// // HRMLabel // // Takes a Human Resource Machine encoded label and optional height, width, and // brush diameter for the output. If not specified, the height is 40, the // width is 3*height, and the brush diameter is height/11. // // Resulting object has .strokes, a list of strokes. Each stroke is a list of // points that should be connected with straight line segments. Each point is // a list of two elements [x,y], scaled to the specified height and width. // // Resulting object also has .extents, which contains the extents // (.extents.min_x, .extents.min_y, .extents.max_x, .extents.max_y) and size // (.extents.width and .extends.height). The extents are the smallest extents // that encompass all of the strokes, including accounting for the brush // diameter. However, the extents will never go outside of the range 0,0 // through width,height; strokes near the edge will be clipped, comforming to // Human Resource Machine's behavior. // // The height, width, and brush diameter, either the defaults or the specified // ones, are available as .height, .width, and .brush_diameter function HRMLabel(encoded_label, height, width, brush_diameter) { "use strict"; function rescale(x, oldmax, newmax) { return x * newmax / oldmax; } this.height = height || 40; this.width = width || this.height*3; this.brush_diameter = brush_diameter || this.height / 11; var hrm_max = 65535; var zlib = window.atob(encoded_label); var zlibu8 = new Uint8Array(zlib.length); for(var i = 0; i < zlib.length; i++) { zlibu8[i] = zlib.charCodeAt(i); } var raw = pako.inflate(zlibu8); var dv = new DataView(raw.buffer); var elements = dv.getUint16(0, true); var points = []; this.strokes = []; var min_x = this.width; var max_x = 0; var min_y = this.height; var max_y = 0; for(var i = 0; i < elements && (i*4+6)<= raw.length; i++) { var index = i*4+4; var x = dv.getUint16(index, true); var y = dv.getUint16(index+2, true); if(x == 0 && y == 0) { this.strokes.push(points); points = []; } else { x = rescale(x, hrm_max, this.width); y = rescale(y, hrm_max, this.height); points.push([x,y]); max_x = Math.max(max_x, x); min_x = Math.min(min_x, x); max_y = Math.max(max_y, y); min_y = Math.min(min_y, y); } } this.extents = { min_x: Math.max(min_x-this.brush_diameter/2, 0), min_y: Math.max(min_y-this.brush_diameter/2, 0), max_x: Math.min(max_x+this.brush_diameter/2, this.width), max_y: Math.min(max_y+this.brush_diameter/2, this.height), } this.extents.width = this.extents.max_x - this.extents.min_x; this.extents.height = this.extents.max_y - this.extents.min_y; } //////////////////////////////////////////////////////////////////////////////// // HRMParser // // Pass in an HRM program in assembly format, either an as array of strings, // with each element representing a single line, or as a single string with // embedded newlines (prefixed with optional carriage returns). // // The "-- HUMAN RESOURCE MACHINE PROGRAM --" header is optional; if present // it will be discarded. // // The returned object will have: // // .comments - Object of the various code comments, indexed on the comment // identifier. So example.comments["3"] retrieves comment 3. // These are the encoded, text form! // // .labels - Same as .comments, but for labels for memory addresses. // // .code - Array of objects describing lines in the source file. Contains: // .code[NUM].cmd - String. The command. One of copyfrom, copyto, // bumpup, bumpdn, jump, jumpn, jumpz, add, sub, // inbox, outbox, comment, blank, asm_comment, // jumpdst, or error // .code[NUM].arg - The argument to the command. Only present for // copyfrom, copyto, bumpup, bumpdn, jump, // jumpn, jumpz, add, sub, comment, asm_comment, // and invalid. For comment it's the identifier // for the image, available through // .comments[.code[NUM].arg]. For asm_comment, // it's the text of the comment, including the // leading "--". For invalid, it's the entire line. // .code[NUM].line_num - Integer. The line number, as Human // Resource Machine counts them. Not present // for invalid, blank, jumpdst, asm_comment or // comment. function HRMParser(lines) { "use strict"; function is_code(cmd) { if(cmd == 'invalid' || cmd == 'blank' || cmd == 'jumpdst' || cmd == 'asm_comment' || cmd == 'comment') { return 0; } return 1; } function tokenize_line(line) { var re_asm_comment = /^--/; var re_jump_dst = /^(\S+):/; var re_whitespace = /\s+/; var match; line = line.replace(/^\s+/, ''); line = line.replace(/\s+$/, ''); if(line == "") { return [ 'blank' ]; } if(match = re_jump_dst.exec(line)) { return ['jumpdst', match[1]]; } if(re_asm_comment.test(line)) { return ['asm_comment', line]; } var tokens = line.split(re_whitespace); var cmd = tokens[0].toLowerCase(); var onearg = ['copyfrom', 'copyto', 'bumpup', 'bumpdn', 'jump', 'jumpn', 'jumpz', 'add', 'sub', 'comment']; var zeroarg = ['inbox', 'outbox']; if(tokens.length == 2) { for(var i = 0; i < onearg.length; i++) { if(cmd == onearg[i]) { return [cmd, tokens[1]]; } } } else if(tokens.length == 1) { for(var i = 0; i < zeroarg.length; i++) { if(cmd == zeroarg[i]) { return [cmd]; } } } return [ 'invalid', line ]; } if((typeof lines) === "string") { lines = lines.split(/\r?\n/); } // Discard header. var line_number_offset = 0; if(lines[0] == "-- HUMAN RESOURCE MACHINE PROGRAM --") { lines.shift(); line_number_offset++; } var parts = this.extract_labels(lines); this.comments = parts.labels['comment']; this.labels = parts.labels['label']; var asm_lines = parts.olines; this.code = []; var code_line_num = 0; var tokens; var lineobj; for(var line = 0; line < asm_lines.length; line++) { tokens = tokenize_line(asm_lines[line]); lineobj = { cmd: tokens[0], src_line_num: line + line_number_offset }; if(is_code(lineobj.cmd)) { code_line_num++; lineobj.line_num = code_line_num; } if(tokens.length == 2) { lineobj.arg = tokens[1]; } this.code.push(lineobj); } } // Given an array of strings representing lines in an HRM program, // break out the graphic labels. Returns an object: // .olines - The input lines minus any used by the labels. // .labels['comment'][NUMBER] = encoded_comment_number // .labels['label'][NUMBER] = encoded_label_number (the memory address) HRMParser.prototype.extract_labels = function(ilines) { "use strict"; var out = {}; out.olines = []; out.labels = {}; out.labels['comment'] = {}; out.labels['label'] = {}; for(var i = 0; i < ilines.length; i++) { var thisline = ilines[i]; var match //console.log(i,thisline); if(match = this.re_define.exec(thisline)) { //console.log('hit'); var body = ''; var more = 1; while(more) { i++; var line = ilines[i]; line = line.replace(/^\s+/, ''); line = line.replace(/\s+$/, ''); if(/;$/.test(line)) { line = line.replace(/;$/, ''); more = 0; } body += line; if(i >= ilines.length) { more = 0; } } var mytype = match[1].toLowerCase(); out.labels[mytype][match[2]] = body; //console.log(mytype,">"+match[2]+"->",body); } else { //console.log('miss'); out.olines.push(thisline); } } return out; } HRMParser.prototype.re_define = /^DEFINE\s+(\S+)\s+(\S+)/i; //////////////////////////////////////////////////////////////////////////////// // HRMViewer // // Inserts a view of Human Resource Machine assembly into current document. // Take the id of an HTML element into which the view should be inserted, as // well as either the HRM assembly as a single string a URL to HRM assembly. // Relative URLs should work. If it's HRM assembly, not a URL, the assembly // _must_ contain at least one newline; it's used to identify it. Conversely, // a URL may not contain any newlines. function HRMViewer(id, source) { "use strict"; // If nothing is passed in, assume the user will call // download_and_append_code_table or append_code_table themselves, although // that's deprecated. if(id === undefined) { return; } if(source.indexOf("\n") >= 0) { this.append_code_table(id, source); } else { this.download_and_append_code_table(id, source); } } HRMViewer.prototype.simple_svg = function(width, height, view_min_x, view_min_y, view_width, view_height) { "use strict"; var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute('version', '1.1'); svg.setAttribute('baseProfile', 'full'); if(typeof view_min_x == 'undefined') { svg.setAttribute('width', width); svg.setAttribute('height', height); } else { svg.setAttribute('viewBox', view_min_x + " " + view_min_y + " " + view_width + " " + view_height); } svg.setAttribute('xmlns', "http://www.w3.org/2000/svg"); this.svg = svg; this.new_el = function(name) { return document.createElementNS(this.svg.namespaceURI,name); } var marker_size = 2.75; var path = this.new_el('path'); path.setAttribute('d', "M0,0"+" "+ "L0,"+marker_size+" "+ "L"+marker_size+","+(marker_size/2)+" "+ "Z"); //path.setAttribute('stroke', 'red'); path.setAttribute('stroke-width', 0); path.setAttribute('class', 'jumppatharrow'); var marker = this.new_el('marker'); marker.setAttribute('id', 'markerArrow'); marker.setAttribute('markerWidth', marker_size); marker.setAttribute('markerHeight', marker_size); marker.setAttribute('refX', '0'); marker.setAttribute('refY', marker_size/2); marker.setAttribute('orient', 'auto'); marker.setAttribute('markerUnits', 'strokeWidth'); marker.appendChild(path); var defs = this.new_el('defs'); defs.appendChild(marker); this.svg.appendChild(defs); this.rect = function(x,y,width,height, color) { var e = this.new_el('rect'); e.setAttribute('x',x); e.setAttribute('y',y); e.setAttribute('width',width); e.setAttribute('height',height); e.setAttribute('fill',color); this.svg.appendChild(e); } this.circle = function(x, y, radius, newclass) { var e = this.new_el('circle'); e.setAttribute('cx',x); e.setAttribute('cy',y); e.setAttribute('r',radius); e.setAttribute('class',newclass); this.svg.appendChild(e); } this.polyline = function(points, width, newclass) { var e = this.new_el('polyline'); var pts = ""; for(var i = 0; i < points.length; i++) { pts += points[i][0] + " " + points[i][1] + " "; } e.setAttribute('points', pts); e.setAttribute('class', newclass); e.setAttribute('fill', 'transparent'); e.setAttribute('stroke-width', width); this.svg.appendChild(e); } this.path = function(command, thisclass) { var e = this.new_el('path'); e.setAttribute('d', command); e.setAttribute('class', thisclass); e.setAttribute('style', 'marker-end: url(#markerArrow)'); this.svg.appendChild(e); } } //////////////////////////////////////////////////////////////////////////////// HRMViewer.prototype.new_hrm_label_svg = function(enclabel) { "use strict"; var label = new HRMLabel(enclabel); var new_svg = new this.simple_svg(label.width, label.height, label.extents.min_x, label.extents.min_y, label.extents.width, label.extents.height); for(var i = 0; i < label.strokes.length; i++) { var points = label.strokes[i]; if(points.length == 0) { } else if(points.length == 1) { new_svg.circle(points[0][0], points[0][1], label.brush_diameter/2, 'stroke'); } else { new_svg.polyline(points, label.brush_diameter, 'stroke'); } } return new_svg.svg; } //////////////////////////////////////////////////////////////////////////////// HRMViewer.prototype.create_jump_diagram = function(width, height, offset_left, offset_top, srcs, dsts) { "use strict"; var new_svg = new this.simple_svg(width, height); //new_svg.rect(0,0,table_width,table_height, 'green'); var max_start_x = 0; var gaps = []; for(var i = 0; i < srcs.length; i++) { var src = srcs[i]['el']; var start_x = src.offset().left + src.outerWidth() - offset_left; if(max_start_x < start_x) { max_start_x = start_x; } var sy = src.offset().top; var label = srcs[i]['dst']; if(label in dsts) { var dst = dsts[label]; var dy = dst.offset().top; gaps.push(Math.abs(dy-sy)); } } gaps.sort(function(a,b){ return a-b; }); // A "transit" is the point on the arc furthest to the right. We try to // space them out evenly. var first_transit = max_start_x + 0; var transit_width = 10; var last_transit = width - transit_width; var num_transits = Math.floor((last_transit - first_transit)/transit_width + 1); var transit_breaks = []; var gaps_per_transit = gaps.length / num_transits; for(var i = 0; i < num_transits; i++) { transit_breaks.push(gaps[Math.floor(gaps_per_transit*i)]); } //console.log("transits",num_transits); //console.log("gaps_per_transit",gaps_per_transit); //console.log("gaps", gaps); //console.log("transit_breaks", transit_breaks); for(var i = 0; i < srcs.length; i++) { var label = srcs[i]['dst']; var src = srcs[i]['el']; if(label in dsts) { var dst = dsts[label]; var startx = src.offset().left-offset_left + src.outerWidth(); var starty = src.offset().top-offset_top + src.outerHeight()/2; var endx = dst.offset().left-offset_left + dst.outerWidth() + 25; var endy = dst.offset().top-offset_top + dst.outerHeight()/2; var gap_y = Math.abs(endy-starty); var transit = 0; for(transit = 0; transit < transit_breaks.length; transit++) { if(gap_y < transit_breaks[transit]) { break; } } var mid_x = first_transit + transit*transit_width; //console.log("(gap:",gap_y,")",first_transit,"+",transit,"*",transit_width,"=",mid_x); var mid_y = (starty + endy) / 2; var bcurve_y = (starty - endy) / 2; var path_cmd = ["M", startx, starty, "C", startx + 20, starty, mid_x, mid_y + bcurve_y, mid_x, mid_y, "C", mid_x, mid_y - bcurve_y, endx + 20, endy, endx, endy ].join(" "); new_svg.path(path_cmd,'jumppath'); } else { console.log("jump label", label, "lacks a matching destination"); } } return new_svg.svg; } HRMViewer.prototype.append_code_table = function(id, data) { "use strict"; this.root_div = $('#'+id); this.root_div.empty(); var parser = new HRMParser(data); this.root = $(document.createElement('table')); // fe0e means "render preceeding as text, do not substitute a color emoji. // Fixes overly helpful behavior on Safari. var rightarrow = '➡\ufe0e'; this.dsts = {}; this.srcs = []; this.line_to_row = {}; var num_len = 2; var pad = "00000"; var code_lines = parser.code.length; if(code_lines.length > 9999) { num_len = 5; } else if(code_lines.length > 999) { num_len = 4; } else if(code_lines.length > 99) { num_len = 3; } var line_number = 0; for(var i = 0; i < parser.code.length; i++) { var linecode = parser.code[i]; var cmd = linecode.cmd; var arg = linecode.arg; if(cmd == 'blank') { continue; } var newclass = cmd; var text = cmd; var jmpdst; if(cmd == 'bumpup') { text = 'bump +'; } else if(cmd == 'bumpdn') { text = 'bump −'; } // else if(cmd == 'inbox') { text = rightarrow + ' inbox'; } // else if(cmd == 'outbox') { text = 'outbox ' + rightarrow; } else if(cmd == 'inbox') { text = 'inbox'; } else if(cmd == 'outbox') { text = 'outbox'; } else if(cmd == 'asm_comment') { text = arg; arg = undefined; } else if(cmd == 'jumpdst') { text = arg; arg = undefined; } else if(cmd == 'jump' || cmd == 'jumpn' || cmd == 'jumpz') { jmpdst = arg; arg = undefined; } var comment_id; if(cmd == 'comment') { comment_id = arg; if(comment_id in parser.comments) { text = ''; arg = undefined; } } var e_cmd = $(document.createElement('span')); if(cmd == "jumpn" || cmd == "jumpz") { e_cmd.append(document.createTextNode("jump")); var overunder = $(document.createElement('div')); overunder.addClass("jumptype"); overunder.append(document.createTextNode("if")); overunder.append(document.createElement('br')); if(text == "jumpn") { overunder.append(document.createTextNode("negative")); } else if(text == "jumpz") { overunder.append(document.createTextNode("zero")); } else { overunder.append(document.createTextNode("unknown")); } e_cmd.append(overunder); } else { e_cmd.text(text); } e_cmd.addClass(newclass); e_cmd.addClass('cmd'); if(cmd == 'jumpdst') { this.dsts[text] = e_cmd; } if(cmd == "comment") { if(comment_id in parser.comments) { var svg = this.new_hrm_label_svg(parser.comments[comment_id]); svg = $(svg); e_cmd.append(svg); } } var re_memory_addr = /^\d+$/; var e_arg = 0; if(arg !== undefined) { e_arg = $(document.createElement('span')); e_arg.addClass(newclass); e_arg.addClass('arg'); var tmp; if(re_memory_addr.test(arg)) { if(arg in parser.labels) { var svg = this.new_hrm_label_svg(parser.labels[arg]); svg = $(svg); e_arg.append(svg); } else { e_arg.text(arg); } } else if(tmp = /\[(\d+)\]/.exec(arg)) { var num = tmp[1]; if(num in parser.labels) { e_arg.append(document.createTextNode("[ ")); var svg = this.new_hrm_label_svg(parser.labels[num]); svg = $(svg); e_arg.append(svg); e_arg.append(document.createTextNode(" ]")); } else { e_arg.text(arg); } } else { e_arg.text(arg); } } if(newclass=='jump' || newclass=='jumpn' || newclass=="jumpz") { this.srcs.push({dst:jmpdst, el:e_cmd}); } var new_td_num = $(document.createElement('td')); if(linecode.line_num) { var linenum = (pad+linecode.line_num).slice(-num_len); new_td_num.text(linenum); } new_td_num.addClass('linenum'); var new_td_code = $(document.createElement('td')); new_td_code.append(e_cmd); if(e_arg) { new_td_code.append($(document.createTextNode(' '))); new_td_code.append(e_arg); } var new_row = $(document.createElement('tr')); new_row.append(new_td_num); new_row.append(new_td_code); this.root.append(new_row); this.line_to_row[linecode.src_line_num] = new_row; } this.root_div.append(this.root); var that=this; setTimeout(function(){that.updateJumpArrows()}, 10); } HRMViewer.prototype.updateJumpArrows = function() { "use strict"; if(this.svg) { this.svg.remove(); } var table_width = this.root.outerWidth() - 70; var table_height = this.root.outerHeight(); this.svg = this.create_jump_diagram( table_width + 50, table_height, this.root_div.offset().left, this.root_div.offset().top, this.srcs, this.dsts); this.root_div.append(this.svg); } // Always clears the current active line. If line_num // is defined, that line will be highlighted by adding // the "active" class to the HRMViewer.prototype.setActiveLine = function(line_num) { if(this.active_row) { this.active_row.removeClass("active"); } if(line_num === undefined) { return; } this.active_row = this.line_to_row[line_num]; if(this.active_row) { this.active_row.addClass("active"); } } HRMViewer.prototype.download_and_append_code_table = function (id, url) { "use strict"; var t = this; function code_arrived(data) { t.append_code_table(id, data); } function failure(xhr,tstatus,err) { $('#'+id).empty(); $('#'+id).text("Error loading "+url+". " + tstatus + " " + err); } $.ajax({ url: url, success: code_arrived, error: failure, dataType: 'text', }); } // Backward compatibility interface. Deprecated. Prefer HRMViewer. var hrm_viewer = HRMViewer;