NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name GitHub Font Preview // @version 1.0.26 // @description A userscript that adds a font file preview // @license MIT // @author Rob Garrison // @namespace https://github.com/Mottie // @match https://github.com/* // @run-at document-idle // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect github.com // @connect githubusercontent.com // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163 // @require https://greasyfork.org/scripts/20469-opentype-js/code/opentypejs.js?version=130870 // @icon https://github.githubassets.com/pinned-octocat.svg // @updateURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-font-preview.user.js // @downloadURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-font-preview.user.js // @supportURL https://github.com/Mottie/GitHub-userscripts/issues // ==/UserScript== /* global opentype */ (() => { "use strict"; let font; let showUnicode = GM_getValue("gfp-show-unicode", false); let showPoints = GM_getValue("gfp-show-points", true); let showArrows = GM_getValue("gfp-show-arrows", true); let currentIndex = 0; // supported font types const fontExt = /\.(otf|ttf|woff)$/i; // canvas colors const glyphFillColor = "#808080"; // (big) (mini) fill color const bigGlyphStrokeColor = "#111111"; // (big) stroke color const bigGlyphMarkerColor = "#f00"; // (big) min & max width marker const miniGlyphMarkerColor = "#606060"; // (mini) glyph index (bottom left corner) const glyphRulerColor = "#a0a0a0"; // (mini) min & max width marker & (big) glyph horizontal lines function startLoad() { const block = $(".blob-wrapper a[href*='?raw=true']"); const body = block && block.closest(".Box-body"); if (body) { body.classList.add("ghfp-body"); body.innerHTML = "<span class='gfp-loading ghd-invert'></span>"; } return block && block.href; } function getFont() { const url = startLoad(); if (url) { // add loading indicator GM_xmlhttpRequest({ method: "GET", url, responseType: "arraybuffer", onload: response => { setupFont(response.response); } }); } } function setupFont(data) { const block = $(".ghfp-body"); const el = $(".final-path"); if (block && el) { try { font = opentype.parse(data); addHTML(block, el); showErrorMessage(""); onFontLoaded(font); } catch (err) { block.innerHTML = "<h2 class='gfp-message cdel'></h2>"; showErrorMessage(err.toString()); if (err.stack) { console.error(err.stack); } throw (err); } } } function addHTML(block, el) { let name = el.textContent || ""; block.innerHTML = ` <div id="gfp-wrapper"> <span class="gfp-info" id="gfp-font-name">${name}</span> <h2 class="gfp-message cdel"></h2> <hr> <div id="gfp-font-data"> <div class="gfp-collapsed">Font Header table <a href="https://www.microsoft.com/typography/OTSPEC/head.htm" target="_blank">head</a></div> <dl id="gfp-head-table"><dt>Undefined</dt></dl> <div class="gfp-collapsed">Horizontal Header table <a href="https://www.microsoft.com/typography/OTSPEC/hhea.htm" target="_blank">hhea</a></div> <dl id="gfp-hhea-table"><dt>Undefined</dt></dl> <div class="gfp-collapsed">Maximum Profile table <a href="https://www.microsoft.com/typography/OTSPEC/maxp.htm" target="_blank">maxp</a></div> <dl id="gfp-maxp-table"><dt>Undefined</dt></dl> <div class="gfp-collapsed">Naming table <a href="https://www.microsoft.com/typography/OTSPEC/name.htm" target="_blank">name</a></div> <dl id="gfp-name-table"><dt>Undefined</dt></dl> <div class="gfp-collapsed">OS/2 and Windows Metrics table <a href="https://www.microsoft.com/typography/OTSPEC/os2.htm" target="_blank">OS/2</a></div> <dl id="gfp-os2-table"><dt>Undefined</dt></dl> <div class="gfp-collapsed">PostScript table <a href="https://www.microsoft.com/typography/OTSPEC/post.htm" target="_blank">post</a></div> <dl id="gfp-post-table"><dt>Undefined</dt></dl> <div class="gfp-collapsed">Character To Glyph Index Mapping Table <a href="https://www.microsoft.com/typography/OTSPEC/cmap.htm" target="_blank">cmap</a></div> <dl id="gfp-cmap-table"><dt>Undefined</dt></dl> <div class="gfp-collapsed">Font Variations table <a href="https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html" target="_blank">fvar</a></div> <dl id="gfp-fvar-table"><dt>Undefined</dt></dl> </div> <hr> <div> <div>Show unicode: <input class="gfp-show-unicode" type="checkbox"${showUnicode ? " checked" : ""}></div> Glyphs <span id="gfp-pagination"></span> <br> <div id="gfp-glyph-list-end"></div> </div> <div style="position: relative"> <div id="gfp-glyph-display"> <canvas id="gfp-glyph-bg" class="ghd-invert" width="500" height="500"></canvas> <canvas id="gfp-glyph" class="ghd-invert" width="500" height="500"></canvas> </div> <div id="gfp-glyph-data"></div> <div style="clear: both"></div> </div> <span style="font-size:0.8em">Powered by <a href="https://github.com/nodebox/opentype.js">opentype.js</a></span> </div> `; prepareGlyphList(); // Add bindings for collapsible font data let tableHeaders = document.getElementById("gfp-font-data").getElementsByTagName("div"), indx = tableHeaders.length; while (indx--) { tableHeaders[indx].addEventListener("click", event => { event.target && event.target.classList.toggle("gfp-collapsed"); }, false); } addBindings(); } function addBindings() { $(".gfp-show-unicode").addEventListener("change", function() { showUnicode = this.checked; GM_setValue("gfp-show-unicode", showUnicode); displayGlyphPage(pageSelected); return false; }, false); $("#gfp-glyph-data").addEventListener("change", function() { showPoints = $(".gfp-show-points", this).checked; showArrows = $(".gfp-show-arrows", this).checked; GM_setValue("gfp-show-points", showPoints); GM_setValue("gfp-show-arrows", showArrows); cellSelect(); return false; }, false); } function $(selector, el) { return (el || document).querySelector(selector); } function init() { // get file name from bread crumb let el = $(".final-path"); // font extension supported? if (el && fontExt.test(el.textContent || "")) { getFont(); } } document.addEventListener("ghmo:container", init); init(); /* Code modified from http://opentype.js.org/ demos */ GM_addStyle(` #gfp-wrapper { text-align:left; padding:20px; } #gfp-wrapper canvas { background-image:none !important; background-color:transparent !important; } .gfp-message { position:relative; top:-3px; padding:1px 5px; font-weight:bold; border-radius:2px; display:none; clear:both; } #gfp-glyphs { width:950px; } .gfp-info { float:right; font-size:14px; color:#999; } #gfp-wrapper hr { clear:both; border:none; border-bottom:1px solid #ccc; margin:20px 0 20px 0; padding:0; } /* Font Inspector */ #gfp-font-data div { font-weight:normal; margin:0; cursor:pointer; } #gfp-font-data div:before { font-size:85%; content:"▼"; display:inline-block; margin-right:6px; transform:unset; } #gfp-font-data .gfp-collapsed:before { transform:rotate(-90deg); } #gfp-font-data div.gfp-collapsed + dl { display:none; } #gfp-font-data dl { margin-top:0; padding-left:2em; color:#777; } #gfp-font-data dt { float:left; } #gfp-font-data dd { margin-left: 12em; word-break:break-all; max-height:100px; overflow-y:auto; } #gfp-font-data .gfp-langtag { font-size:85%; color:#999; white-space:nowrap; } #gfp-font-data .gfp-langname { padding-right:0.5em; } #gfp-font-data .gfp-underline { border-bottom:1px solid #555; } /* Glyph Inspector */ #gfp-pagination a { margin:0 0.3em; cursor:pointer; } #gfp-pagination .gfp-page-selected { font-weight:bold; cursor:default; -webkit-filter:brightness(150%); filter:brightness(150%); } canvas.gfp-item { float:left; border:solid 1px #a0a0a0; margin-right:-1px; margin-bottom:-1px; cursor:pointer; } canvas.gfp-item:hover { opacity:.8; } #gfp-glyph-list-end { clear:both; height:20px; } #gfp-glyph-display { float:left; border:solid 1px #a0a0a0; position:relative; width:500px; height:500px; } #gfp-glyph, #gfp-glyph-bg { position:absolute; top:0; left:0; border:0; } #gfp-glyph-data { float:left; margin-left:2em; } #gfp-glyph-data dl { margin:0; } #gfp-glyph-data dt { float:left; } #gfp-glyph-data dd { margin-left:12em; } #gfp-glyph-data pre { font-size:11px; } pre.gfp-path { margin:0; } pre.gfp-contour { margin:0 0 1em 2em; border-bottom:solid 1px #a0a0a0; } span.gfp-oncurve { color:var(--color-scale-blue-6); } span.gfp-offcurve { color:var(--color-scale-red-6); } .gfp-loading { display:block; margin:20px auto; border-radius:50%; border-width:2px; border-style:solid; border-color: transparent transparent #000 #000; width:30px; height:30px; animation:gfploading .5s infinite linear; } @keyframes gfploading { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `); /*eslint-disable */ /* Code copied from http://opentype.js.org/font-inspector.html */ function escapeHtml(unsafe) { return unsafe .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/\u0022/g, '"') .replace(/\u0027/g, '''); } function displayNames(names) { let indx, property, translations, langs, lang, langIndx, langLen, esclang, html = '', properties = Object.keys(names), len = properties.length; for (indx = 0; indx < len; indx++) { property = properties[indx]; html += '<dt>' + escapeHtml(property) + '</dt><dd>'; translations = names[property]; langs = Object.keys(translations); langLen = langs.length; for (langIndx = 0; langIndx < langLen; langIndx++) { lang = langs[langIndx]; esclang = escapeHtml(lang); html += '<span class="gfp-langtag">' + esclang + '</span> <span class="gfp-langname" lang=' + esclang + '>' + escapeHtml(translations[lang]) + '</span> '; } html += '</dd>'; } document.getElementById('gfp-name-table').innerHTML = html; } function displayFontData() { let html, tablename, table, property, value, element; for (tablename in font.tables) { if (font.tables.hasOwnProperty(tablename)) { table = font.tables[tablename]; if (tablename === 'name') { displayNames(table); continue; } html = ''; for (property in table) { if (table.hasOwnProperty(property)) { value = table[property]; html += '<dt>' + property + '</dt><dd>'; if (Array.isArray(value) && typeof value[0] === 'object') { html += value.map(item => { return JSON.stringify(item); }).join('<br>'); } else if (typeof value === 'object') { html += JSON.stringify(value); } else { html += value; } html += '</dd>'; } } element = document.getElementById('gfp-' + tablename + '-table'); if (element) { element.innerHTML = html; } } } } /* Code copied from http://opentype.js.org/glyph-inspector.html */ const cellCount = 100, cellWidth = 62, cellHeight = 60, cellMarginTop = 1, cellMarginBottom = 8, cellMarginLeftRight = 1, glyphMargin = 5, pixelRatio = window.devicePixelRatio || 1, arrowLength = 10, arrowAperture = 4; let pageSelected, fontScale, fontSize, fontBaseline, glyphScale, glyphSize, glyphBaseline; function enableHighDPICanvas(canvas) { let pixelRatio, oldWidth, oldHeight; if (typeof canvas === 'string') { canvas = document.getElementById(canvas); } pixelRatio = window.devicePixelRatio || 1; if (pixelRatio === 1) { return; } oldWidth = canvas.width; oldHeight = canvas.height; canvas.width = oldWidth * pixelRatio; canvas.height = oldHeight * pixelRatio; canvas.style.width = oldWidth + 'px'; canvas.style.height = oldHeight + 'px'; canvas.getContext('2d').scale(pixelRatio, pixelRatio); } function showErrorMessage(message) { let el = $('.gfp-message'); el.style.display = (!message || message.trim().length === 0) ? 'none' : 'block'; el.innerHTML = message; } function pathCommandToString(cmd) { let str = '<strong>' + cmd.type + '</strong> ' + ((cmd.x !== undefined) ? 'x=' + cmd.x + ' y=' + cmd.y + ' ' : '') + ((cmd.x1 !== undefined) ? 'x1=' + cmd.x1 + ' y1=' + cmd.y1 + ' ' : '') + ((cmd.x2 !== undefined) ? 'x2=' + cmd.x2 + ' y2=' + cmd.y2 : ''); return str; } function contourToString(contour) { return '<pre class="gfp-contour">' + contour.map(point => { return '<span class="gfp-' + (point.onCurve ? 'oncurve' : 'offcurve') + '">x=' + point.x + ' y=' + point.y + '</span>'; }).join('\n') + '</pre>'; } function formatUnicode(unicode) { unicode = unicode.toString(16); if (unicode.length > 4) { return ('000000' + unicode.toUpperCase()).substr(-6); } else { return ('0000' + unicode.toUpperCase()).substr(-4); } } function displayGlyphData(glyphIndex) { let glyph, contours, html, container = document.getElementById('gfp-glyph-data'), addItem = name => { return glyph[name] ? `<dt>${name}</dt><dd>${glyph[name]}</dd>` : ''; }; if (glyphIndex < 0) { container.innerHTML = ''; return; } glyph = font.glyphs.get(glyphIndex); html = `<dl> <dt>Show points</dt> <dd><input class="gfp-show-points" type="checkbox"${showPoints ? ' checked' : ''}></dd> <dt>Show arrows</dt> <dd><input class="gfp-show-arrows" type="checkbox"${showArrows ? ' checked' : ''}></dd> <dt>name</dt><dd>${glyph.name}</dd>`; if (glyph.unicode) { html += '<dt>unicode</dt><dd>' + glyph.unicodes.map(formatUnicode).join(', ') + '</dd>'; } html += addItem('index') + addItem('xMin') + addItem('xMax') + addItem('yMin') + addItem('yMax') + addItem('advanceWidth') + addItem('leftSideBearing') + '</dl>'; if (glyph.numberOfContours > 0) { contours = glyph.getContours(); html += 'contours:<br>' + contours.map(contourToString).join('\n'); } else if (glyph.isComposite) { html += '<br>This composite glyph is a combination of :<ul><li>' + glyph.components.map(component => { return 'glyph ' + component.glyphIndex + ' at dx=' + component.dx + ', dy=' + component.dy; }).join('</li><li>') + '</li></ul>'; } else if (glyph.path) { html += 'path:<br><pre class="gfp-path"> ' + glyph.path.commands.map(pathCommandToString).join('\n ') + '\n</pre>'; } container.innerHTML = html; } function drawArrow(ctx, x1, y1, x2, y2) { let dx = x2 - x1, dy = y2 - y1, segmentLength = Math.sqrt(dx * dx + dy * dy), unitx = dx / segmentLength, unity = dy / segmentLength, basex = x2 - arrowLength * unitx, basey = y2 - arrowLength * unity, normalx = arrowAperture * unity, normaly = -arrowAperture * unitx; ctx.beginPath(); ctx.moveTo(x2, y2); ctx.lineTo(basex + normalx, basey + normaly); ctx.lineTo(basex - normalx, basey - normaly); ctx.lineTo(x2, y2); ctx.closePath(); ctx.fill(); } /** * This function is Path.prototype.draw with an arrow * at the end of each contour. */ function drawPathWithArrows(ctx, path) { let indx, cmd, x1, y1, x2, y2, arrows = [], len = path.commands.length; ctx.beginPath(); for (indx = 0; indx < len; indx++) { cmd = path.commands[indx]; if (cmd.type === 'M') { if (x1 !== undefined) { arrows.push([ctx, x1, y1, x2, y2]); } ctx.moveTo(cmd.x, cmd.y); } else if (cmd.type === 'L') { ctx.lineTo(cmd.x, cmd.y); x1 = x2; y1 = y2; } else if (cmd.type === 'C') { ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); x1 = cmd.x2; y1 = cmd.y2; } else if (cmd.type === 'Q') { ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y); x1 = cmd.x1; y1 = cmd.y1; } else if (cmd.type === 'Z') { arrows.push([ctx, x1, y1, x2, y2]); ctx.closePath(); } x2 = cmd.x; y2 = cmd.y; } if (path.fill) { ctx.fillStyle = path.fill; ctx.fill(); } if (path.stroke) { ctx.strokeStyle = path.stroke; ctx.lineWidth = path.strokeWidth; ctx.stroke(); } ctx.fillStyle = bigGlyphStrokeColor; if (showArrows) { arrows.forEach(arrow => { drawArrow.apply(null, arrow); }); } } function displayGlyph(glyphIndex) { let glyph, glyphWidth, xmin, xmax, x0, markSize, path, canvas = document.getElementById('gfp-glyph'), ctx = canvas.getContext('2d'), width = canvas.width / pixelRatio, height = canvas.height / pixelRatio; ctx.clearRect(0, 0, width, height); if (glyphIndex < 0) { return; } glyph = font.glyphs.get(glyphIndex); glyphWidth = glyph.advanceWidth * glyphScale; xmin = (width - glyphWidth) / 2; xmax = (width + glyphWidth) / 2; x0 = xmin; markSize = 10; ctx.fillStyle = bigGlyphMarkerColor; ctx.fillRect(xmin - markSize + 1, glyphBaseline, markSize, 1); ctx.fillRect(xmin, glyphBaseline, 1, markSize); ctx.fillRect(xmax, glyphBaseline, markSize, 1); ctx.fillRect(xmax, glyphBaseline, 1, markSize); ctx.textAlign = 'center'; ctx.fillText('0', xmin, glyphBaseline + markSize + 10); ctx.fillText(glyph.advanceWidth, xmax, glyphBaseline + markSize + 10); ctx.fillStyle = bigGlyphStrokeColor; path = glyph.getPath(x0, glyphBaseline, glyphSize); path.fill = glyphFillColor; path.stroke = bigGlyphStrokeColor; path.strokeWidth = 1.5; drawPathWithArrows(ctx, path); if (showPoints) { glyph.drawPoints(ctx, x0, glyphBaseline, glyphSize); } } function renderGlyphItem(canvas, glyphIndex) { const cellMarkSize = 4, ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, cellWidth, cellHeight); if (glyphIndex >= font.numGlyphs) { return; } ctx.fillStyle = miniGlyphMarkerColor; ctx.font = '10px sans-serif'; let glyph = font.glyphs.get(glyphIndex), glyphWidth = glyph.advanceWidth * fontScale, xmin = (cellWidth - glyphWidth) / 2, xmax = (cellWidth + glyphWidth) / 2, x0 = xmin; ctx.fillText(showUnicode ? glyph.unicodes.map(formatUnicode).join(', ') : glyphIndex, 1, cellHeight - 1); ctx.fillStyle = glyphRulerColor; ctx.fillRect(xmin - cellMarkSize + 1, fontBaseline, cellMarkSize, 1); ctx.fillRect(xmin, fontBaseline, 1, cellMarkSize); ctx.fillRect(xmax, fontBaseline, cellMarkSize, 1); ctx.fillRect(xmax, fontBaseline, 1, cellMarkSize); ctx.fillStyle = '#000000'; let path = glyph.getPath(x0, fontBaseline, fontSize); path.fill = glyphFillColor; path.draw(ctx); } function displayGlyphPage(pageNum) { pageSelected = pageNum; const last = $('.gfp-page-selected'); if (last) last.className = ''; document.getElementById('gfp-p' + pageNum).className = 'gfp-page-selected'; let indx, firstGlyph = pageNum * cellCount; for (indx = 0; indx < cellCount; indx++) { renderGlyphItem(document.getElementById('gfp-g' + indx), firstGlyph + indx); } } function pageSelect(event) { displayGlyphPage((event.target.id || '').replace('gfp-p', '')); } function initGlyphDisplay() { let glyphBgCanvas = document.getElementById('gfp-glyph-bg'), w = glyphBgCanvas.width / pixelRatio, h = glyphBgCanvas.height / pixelRatio, glyphW = w - glyphMargin * 2, glyphH = h - glyphMargin * 2, head = font.tables.head, maxHeight = head.yMax - head.yMin, ctx = glyphBgCanvas.getContext('2d'); glyphScale = Math.min(glyphW / (head.xMax - head.xMin), glyphH / maxHeight); glyphSize = glyphScale * font.unitsPerEm; glyphBaseline = glyphMargin + glyphH * head.yMax / maxHeight; function hline(text, yunits) { let ypx = glyphBaseline - yunits * glyphScale; ctx.fillText(text, 2, ypx + 3); ctx.fillRect(80, ypx, w, 1); } ctx.clearRect(0, 0, w, h); ctx.fillStyle = glyphRulerColor; hline('Baseline', 0); hline('yMax', font.tables.head.yMax); hline('yMin', font.tables.head.yMin); hline('Ascender', font.tables.hhea.ascender); hline('Descender', font.tables.hhea.descender); hline('Typo Ascender', font.tables.os2.sTypoAscender); hline('Typo Descender', font.tables.os2.sTypoDescender); } function onFontLoaded(font) { let indx, link, lastIndex, w = cellWidth - cellMarginLeftRight * 2, h = cellHeight - cellMarginTop - cellMarginBottom, head = font.tables.head, maxHeight = head.yMax - head.yMin, pagination = document.getElementById('gfp-pagination'), fragment = document.createDocumentFragment(), numPages = Math.ceil(font.numGlyphs / cellCount); fontScale = Math.min(w / (head.xMax - head.xMin), h / maxHeight); fontSize = fontScale * font.unitsPerEm; fontBaseline = cellMarginTop + h * head.yMax / maxHeight; pagination.innerHTML = ''; for (indx = 0; indx < numPages; indx++) { link = document.createElement('a'); lastIndex = Math.min(font.numGlyphs - 1, (indx + 1) * cellCount - 1); link.textContent = indx * cellCount + '-' + lastIndex; link.id = 'gfp-p' + indx; link.addEventListener('click', pageSelect, false); fragment.appendChild(link); // A white space allows to break very long lines into multiple lines. // This is needed for fonts with thousands of glyphs. fragment.appendChild(document.createTextNode(' ')); } pagination.appendChild(fragment); displayFontData(); initGlyphDisplay(); displayGlyphPage(0); displayGlyph(-1); displayGlyphData(-1); } function cellSelect(event) { if (!font) { return; } let firstGlyphIndex = pageSelected * cellCount, cellIndex = event ? +event.target.id.replace('gfp-g', '') : currentIndex, glyphIndex = firstGlyphIndex + cellIndex; currentIndex = cellIndex; if (glyphIndex < font.numGlyphs) { displayGlyph(glyphIndex); displayGlyphData(glyphIndex); } } function prepareGlyphList() { let indx, canvas, marker = document.getElementById('gfp-glyph-list-end'), parent = marker.parentElement; for (indx = 0; indx < cellCount; indx++) { canvas = document.createElement('canvas'); canvas.width = cellWidth; canvas.height = cellHeight; canvas.className = 'gfp-item ghd-invert'; canvas.id = 'gfp-g' + indx; canvas.addEventListener('click', cellSelect, false); enableHighDPICanvas(canvas); parent.insertBefore(canvas, marker); } } /* eslint-enable */ })();