GC7R0TA-Rechenzentrum/hrm/human-resource-machine-viewer/hrm.js

678 lines
20 KiB
JavaScript
Raw Permalink Normal View History

2024-05-04 07:40:23 +00:00
/*
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;