mirror of
https://github.com/ijprest/keyboard-layout-editor.git
synced 2026-04-07 00:31:08 -07:00
Google Fonts now supplies an HTTPS rather than HTTP URL in its snippet, and adds quotes around the URL string. This change allows such snippets to survive the sanitation process.
273 lines
11 KiB
JavaScript
273 lines
11 KiB
JavaScript
var $renderKey = (typeof(exports) !== 'undefined') ? exports : {};
|
|
(function () {
|
|
"use strict";
|
|
|
|
// Some predefined sizes for our caps:
|
|
// - unit == size of 1 unit, e.g., 0.75", or 19.05mm is standard
|
|
// - keySpacing == distance from edge of unit-square to keycap, e.g., (0.75" - 0.715")/2 (for DCS)
|
|
// - bevelMargin == distance from edge of keycap (at bottom) to edge of keycap (at top), e.g., (0.715" - 0.470")/2 (for DCS)
|
|
// - padding == distance between text & edge of keycap
|
|
// - strokeWidth == thickness of the outline strokes
|
|
// - roundInner/roundOuter == corner roundness for inner/outer borders
|
|
var unitSizes = {
|
|
px : {
|
|
unit : 54,
|
|
strokeWidth: 1,
|
|
"" : { profile: "" , keySpacing: 0, bevelMargin: 6, bevelOffsetTop: 3, bevelOffsetBottom: 3, padding: 3, roundOuter: 5, roundInner: 3 },
|
|
"DCS" : { profile: "DCS", keySpacing: 0, bevelMargin: 6, bevelOffsetTop: 3, bevelOffsetBottom: 3, padding: 3, roundOuter: 5, roundInner: 3 },
|
|
"DSA" : { profile: "DSA", keySpacing: 0, bevelMargin: 6, bevelOffsetTop: 0, bevelOffsetBottom: 0, padding: 3, roundOuter: 5, roundInner: 8 },
|
|
"SA" : { profile: "SA", keySpacing: 0, bevelMargin: 6, bevelOffsetTop: 2, bevelOffsetBottom: 2, padding: 3, roundOuter: 5, roundInner: 5 },
|
|
"CHICKLET" : { profile: "CHICKLET", keySpacing: 3, bevelMargin: 1, bevelOffsetTop: 0, bevelOffsetBottom: 2, padding: 4, roundOuter: 4, roundInner: 4 },
|
|
},
|
|
mm : {
|
|
unit: 19.05,
|
|
strokeWidth: 0.20,
|
|
"" : { profile: "" , keySpacing: 0.4445, bevelMargin: 3.1115, padding: 0, roundOuter: 1.0, roundInner: 2.0 },
|
|
"DCS" : { profile: "DCS", keySpacing: 0.4445, bevelMargin: 3.1115, padding: 0, roundOuter: 1.0, roundInner: 2.0 },
|
|
"DSA" : { profile: "DSA", keySpacing: 0.4445, bevelMargin: 3.1115, padding: 0, roundOuter: 1.0, roundInner: 2.0 },
|
|
"SA" : { profile: "SA", keySpacing: 0.4445, bevelMargin: 3.1115, padding: 0, roundOuter: 1.0, roundInner: 2.0 },
|
|
"CHICKLET" : { profile: "CHICKLET", keySpacing: 0.4445, bevelMargin: 3.1115, padding: 0, roundOuter: 1.0, roundInner: 2.0 },
|
|
}
|
|
};
|
|
["px","mm"].forEach(function(unit) {
|
|
["","DCS","DSA", "SA", "CHICKLET"].forEach(function(profile) {
|
|
unitSizes[unit][profile].unit = unitSizes[unit].unit;
|
|
unitSizes[unit][profile].strokeWidth = unitSizes[unit].strokeWidth;
|
|
});
|
|
unitSizes[unit].OEM = unitSizes[unit].DCS; // same, for now
|
|
});
|
|
|
|
// Lighten a color by the specified amount
|
|
function lightenColor(color,mod) {
|
|
var c = $color.sRGB8(color.r,color.g,color.b).Lab();
|
|
c.l = Math.min(100,c.l*mod);
|
|
return c.sRGB8();
|
|
}
|
|
|
|
function getProfile(key) {
|
|
return (/\b(SA|DSA|DCS|OEM|CHICKLET)\b/.exec(key.profile) || [""])[0];
|
|
}
|
|
|
|
function getRenderParms(key, sizes) {
|
|
var parms = {};
|
|
|
|
parms.jShaped = (key.width !== key.width2) || (key.height !== key.height2) || key.x2 || key.y2;
|
|
|
|
// Overall dimensions of the unit square(s) that the cap occupies
|
|
parms.capwidth = sizes.unit * key.width;
|
|
parms.capheight = sizes.unit * key.height;
|
|
parms.capx = sizes.unit * key.x;
|
|
parms.capy = sizes.unit * key.y;
|
|
if(parms.jShaped) {
|
|
parms.capwidth2 = sizes.unit * key.width2;
|
|
parms.capheight2 = sizes.unit * key.height2;
|
|
parms.capx2 = sizes.unit * (key.x + key.x2);
|
|
parms.capy2 = sizes.unit * (key.y + key.y2);
|
|
}
|
|
|
|
// Dimensions of the outer part of the cap
|
|
parms.outercapwidth = parms.capwidth - sizes.keySpacing*2;
|
|
parms.outercapheight = parms.capheight - sizes.keySpacing*2;
|
|
parms.outercapx = parms.capx + sizes.keySpacing;
|
|
parms.outercapy = parms.capy + sizes.keySpacing;
|
|
if(parms.jShaped) {
|
|
parms.outercapy2 = parms.capy2 + sizes.keySpacing;
|
|
parms.outercapx2 = parms.capx2 + sizes.keySpacing;
|
|
parms.outercapwidth2 = parms.capwidth2 - sizes.keySpacing*2;
|
|
parms.outercapheight2 = parms.capheight2 - sizes.keySpacing*2;
|
|
}
|
|
|
|
// Dimensions of the top of the cap
|
|
parms.innercapwidth = parms.outercapwidth - sizes.bevelMargin*2;
|
|
parms.innercapheight = parms.outercapheight - sizes.bevelMargin*2 - (sizes.bevelOffsetBottom-sizes.bevelOffsetTop);
|
|
parms.innercapx = parms.outercapx + sizes.bevelMargin;
|
|
parms.innercapy = parms.outercapy + sizes.bevelMargin - sizes.bevelOffsetTop;
|
|
if(parms.jShaped) {
|
|
parms.innercapwidth2 = parms.outercapwidth2 - sizes.bevelMargin*2;
|
|
parms.innercapheight2 = parms.outercapheight2 - sizes.bevelMargin*2;
|
|
parms.innercapx2 = parms.outercapx2 + sizes.bevelMargin;
|
|
parms.innercapy2 = parms.outercapy2 + sizes.bevelMargin - sizes.bevelOffsetTop;
|
|
}
|
|
|
|
// Dimensions of the text part of the cap
|
|
parms.textcapwidth = parms.innercapwidth - sizes.padding*2;
|
|
parms.textcapheight = parms.innercapheight - sizes.padding*2;
|
|
parms.textcapx = parms.innercapx + sizes.padding;
|
|
parms.textcapy = parms.innercapy + sizes.padding;
|
|
|
|
parms.darkColor = key.color;
|
|
parms.lightColor = lightenColor($color.hex(key.color), 1.2).hex();
|
|
|
|
// Rotation matrix about the origin
|
|
parms.origin_x = sizes.unit * key.rotation_x;
|
|
parms.origin_y = sizes.unit * key.rotation_y;
|
|
var mat = Math.transMatrix(parms.origin_x, parms.origin_y).mult(Math.rotMatrix(key.rotation_angle)).mult(Math.transMatrix(-parms.origin_x, -parms.origin_y));
|
|
|
|
// Construct the *eight* corner points, transform them, and determine the transformed bbox.
|
|
parms.rect = { x:parms.capx, y:parms.capy, w:parms.capwidth, h:parms.capheight, x2:parms.capx+parms.capwidth, y2:parms.capy+parms.capheight };
|
|
parms.rect2 = parms.jShaped ? { x:parms.capx2, y:parms.capy2, w:parms.capwidth2, h:parms.capheight2, x2:parms.capx2+parms.capwidth2, y2:parms.capy2+parms.capheight2 } : parms.rect;
|
|
parms.bbox = { x:9999999, y:9999999, x2:-9999999, y2:-9999999 };
|
|
var corners = [
|
|
{x:parms.rect.x, y:parms.rect.y},
|
|
{x:parms.rect.x, y:parms.rect.y2},
|
|
{x:parms.rect.x2, y:parms.rect.y},
|
|
{x:parms.rect.x2, y:parms.rect.y2}
|
|
];
|
|
if(parms.jShaped) corners.push(
|
|
{x:parms.rect2.x, y:parms.rect2.y},
|
|
{x:parms.rect2.x, y:parms.rect2.y2},
|
|
{x:parms.rect2.x2, y:parms.rect2.y},
|
|
{x:parms.rect2.x2, y:parms.rect2.y2}
|
|
);
|
|
for(var i = 0; i < corners.length; ++i) {
|
|
corners[i] = mat.transformPt(corners[i]);
|
|
parms.bbox.x = Math.min(parms.bbox.x, corners[i].x);
|
|
parms.bbox.y = Math.min(parms.bbox.y, corners[i].y);
|
|
parms.bbox.x2 = Math.max(parms.bbox.x2, corners[i].x);
|
|
parms.bbox.y2 = Math.max(parms.bbox.y2, corners[i].y);
|
|
}
|
|
parms.bbox.w = parms.bbox.x2 - parms.bbox.x;
|
|
parms.bbox.h = parms.bbox.y2 - parms.bbox.y;
|
|
|
|
return parms;
|
|
}
|
|
|
|
var keycap_html, keycap_svg, keyboard_svg;
|
|
$renderKey.init = function() {
|
|
keycap_html = doT.template($('#keycap_html').html(), {__proto__: doT.templateSettings, varname:"key, sizes, parms, $sanitize, lightenColor"});
|
|
keycap_svg = doT.template($('#keycap_svg').html(), {__proto__: doT.templateSettings, varname:"key, sizes, parms, $sanitize, lightenColor", strip:false});
|
|
keyboard_svg = doT.template($('#keyboard_svg').html(), {__proto__: doT.templateSettings, varname:"parms", strip:false});
|
|
};
|
|
|
|
// Given a key, generate the HTML needed to render it
|
|
$renderKey.html = function(key, $sanitize) {
|
|
var sizes = unitSizes.px[getProfile(key)]; // always in pixels
|
|
var parms = getRenderParms(key, sizes);
|
|
|
|
// Update the rects & bounding-box of the key (for click-selection purposes)
|
|
key.rect = parms.rect;
|
|
key.rect2 = parms.rect2;
|
|
key.bbox = parms.bbox;
|
|
|
|
// Keep an inverse transformation matrix so that we can transform mouse coordinates into key-space.
|
|
key.mat = Math.transMatrix(parms.origin_x, parms.origin_y).mult(Math.rotMatrix(-key.rotation_angle)).mult(Math.transMatrix(-parms.origin_x, -parms.origin_y));
|
|
|
|
// Determine the location of the rotation crosshairs for the key
|
|
key.crosshairs = "none";
|
|
if(key.rotation_x || key.rotation_y || key.rotation_angle) {
|
|
key.crosshairs_x = parms.origin_x;
|
|
key.crosshairs_y = parms.origin_y;
|
|
key.crosshairs = "block";
|
|
}
|
|
|
|
// Generate the HTML
|
|
return keycap_html(key, sizes, parms, $sanitize, lightenColor);
|
|
};
|
|
|
|
// Given a key, generate the SVG needed to render it
|
|
$renderKey.svg = function(key, index, bbox, sizes, $sanitize) {
|
|
|
|
// Update bbox
|
|
var parms = getRenderParms(key, sizes);
|
|
bbox.x = Math.min(bbox.x, parms.bbox.x);
|
|
bbox.y = Math.min(bbox.y, parms.bbox.y);
|
|
bbox.x2 = Math.max(bbox.x2, parms.bbox.x2);
|
|
bbox.y2 = Math.max(bbox.y2, parms.bbox.y2);
|
|
parms.index = index;
|
|
|
|
return keycap_svg(key, sizes, parms, $sanitize, lightenColor);
|
|
};
|
|
|
|
$renderKey.fullSVG = function(keys, metadata) {
|
|
// Render all the keys
|
|
var units = "px";
|
|
var bbox = { x: 99999999, y:99999999, x2:-99999999, y2:-99999999 };
|
|
var keysSVG = "";
|
|
keys.forEach(function(key,index) {
|
|
keysSVG += $renderKey.svg(key, index, bbox, unitSizes[units][getProfile(key)]);
|
|
});
|
|
|
|
// Wrap with SVG boilerplate
|
|
var kbdMargin = 10, kbdPadding = 5;
|
|
return keyboard_svg({
|
|
margin: 10,
|
|
padding: 5,
|
|
width: bbox.x2,
|
|
height: bbox.y2,
|
|
units: units,
|
|
backcolor: metadata.backcolor,
|
|
strokeWidth: unitSizes[units].strokeWidth,
|
|
keys: keysSVG
|
|
});
|
|
};
|
|
|
|
$renderKey.getGlyphsFromRules = function(rules) {
|
|
// Find rules that look like the base slyph-set definition
|
|
var classes = [];
|
|
rules.forEach(function(rule) {
|
|
if(!rule.name && rule.selector.length === 1 && rule.selector[0].match(/^\.[a-zA-Z0-9]+$/)) {
|
|
classes.push(rule.selector[0].substring(1));
|
|
}
|
|
});
|
|
|
|
// Find rules that look like glyphs
|
|
var glyphs = [];
|
|
rules.forEach(function(rule) {
|
|
if(!rule.name && rule.selector.length > 0) {
|
|
rule.selector.forEach(function(selector) {
|
|
var matches = selector.match(/^\.([a-zA-Z0-9]+)-([-a-zA-Z0-9]+)\:(before|after)$/);
|
|
if(matches) {
|
|
var theClass = classes.indexOf(matches[1]);
|
|
if(theClass != -1) {
|
|
var glyph = { name: matches[2], html: "<i class='" + classes[theClass] + " " + matches[1]+"-"+matches[2] +"'></i>" };
|
|
glyphs.push(glyph);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
glyphs.sort(function(a,b) { return a.name.localeCompare(b.name); });
|
|
return glyphs;
|
|
}
|
|
|
|
$renderKey.sanitizeCssRules = function(rules) {
|
|
if(rules) {
|
|
// Sanitize the CSS
|
|
rules.forEach(function(rule) {
|
|
if(!rule.name) {
|
|
for(var i = 0; i < rule.selector.length; ++i) {
|
|
if(rule.selector[i] !== "#keyboard-bg") {
|
|
rule.selector[i] = "#keyboard .keycap " + rule.selector[i] + ", #glyphScroller " + rule.selector[i];
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// Re-stringify the sanitized CSS
|
|
var css = "";
|
|
rules.forEach(function(rule) {
|
|
if(!rule.name) {
|
|
css += rule.selector.join(', ') + " { ";
|
|
if(rule.decls) {
|
|
for(var i = 0; i < rule.decls.length; ++i) {
|
|
css += rule.decls[i][0] + ": " + rule.decls[i][1] + "; ";
|
|
}
|
|
}
|
|
css += "}\n";
|
|
} else {
|
|
var ok = (rule.name === "@font-face")
|
|
|| (rule.name === "@import" && !rule.content && rule.selector.match(/^url\('?https?:\/\/fonts.googleapis.com\/css\?family=[^\)]+'?\)$/));
|
|
if(ok) {
|
|
css += rule.name;
|
|
if(rule.selector) css += ' ' + rule.selector;
|
|
if(rule.content) css += '{ ' + rule.content + ' }\n';
|
|
else css += ';\n';
|
|
}
|
|
}
|
|
});
|
|
return css;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
}());
|