GC7R0TA-Rechenzentrum/hrm/human-resource-machine-viewer/hrm.js
2024-05-04 09:44:30 +02:00

678 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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
<http://www.gnu.org/licenses/>.
*/
////////////////////////////////////////////////////////////////////////////////
// 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 <tr>
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;