Skip to content

Instantly share code, notes, and snippets.

@amcgregor
Last active August 4, 2023 19:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amcgregor/0c79746b8391da346c7590ddac6b1881 to your computer and use it in GitHub Desktop.
Save amcgregor/0c79746b8391da346c7590ddac6b1881 to your computer and use it in GitHub Desktop.
Python-like prototype additions, polyfills, and server-backed client-side behaviour.
// console.* Polyfill and, in production, no-op replacement.
(function() {
// An inspired polyfill for standard web browser user agent console interactions.
//
// This serves several purposes:
//
// 1. Add standardized methods missing from the local implementation to prevent code calling these from producing exceptions which halt JavaScript execution.
// 2. Direct certain logging actions at a back-end server collection endpoint, for archival, analytics, and diagnostics.
// 3. Squelch (mute or silence) "noisier" console endpoints to prevent user agents from exposing diagnostic information to end-users.
// Determine if we are running in "local development" or not based on hostname and protocol.
const LOCAL = new RegExp("^" + [
"([^\.]\.)+local", // DNS-SD ("Bonjour") local-network automatic hostnames.
"([^\.]\.)+example", // Reserved for private use.
"([^\.]\.)+test", // Reserved for private use.
"([^\.]\.)+internal", // Reserved for private use.
"([^\.]\.)*localhost", // Loopback interface by name.
"127.0.0.1", // IPv4 loopback interface.
"::1", // IPv6 loopback interface.
].join("|") + "$", "i");
const ISLOCAL = (window.location.protocol == 'file:') || LOCAL.test(window.location.hostname);
const NOOP = () => {};
const COLLECT = (event, ...args) => { fetch(`/meta/collect/${event}`, {
method: 'POST',
mode: 'same-origin',
cache: 'no-cache',
credentials: 'same-origin',
redirect: 'error',
referrer: 'client',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({'args': args}),
}) };
const METHODS = [
'assert',
'clear',
'count',
'countReset',
'debug',
'dir',
'dirxml',
'error',
'exception', // Deprecated! Do not use, prefer `error` instead!
'group',
'groupCollapsed',
'groupEnd',
'info',
'log',
'markTimeline', // Deprecated, WebKit-specific! Use timeStamp instead!
'profile',
'profileEnd',
'table',
'time',
'timeEnd',
'timeLog',
'timeStamp', // Deprecated, removed, and WebKit-specific!
'timeline', // Deprecated!
'timelineEnd', // Deprecated!
'trace',
'warn',
];
const INSTRUMENT = [
'debug',
'error',
'exception',
'info',
'log',
'table',
'trace',
'warn',
];
const SILENCE = [ // In "production" silence these logging levels.
'debug',
'info',
'log',
]
var console = (window.console = window.console || {});
var original = {}; // Needs to be exposed to nested contexts, thus not 'let'.
// Only populate ("polyfill") missing (undefined) methods.
for ( let method of METHODS )
if ( !console[method] )
console[method] = NOOP.bind(console);
// "Silence" certain logging levels/methods in production environments.
if ( !ISLOCAL )
for ( let method of SILENCE )
console[method] = NOOP.bind(console);
// Instrument (interpose) specific methods to route those classes of message to the back-end.
// Do not do so if we are served from the file: protocol, as there is no server to accept these notifications.
if ( window.location.protocol != 'file:' )
for ( let method of INSTRUMENT ) {
original[method] = console[method];
if ( SILENCE.includes(method) )
console[method] = COLLECT.bind(console, method);
else
console[method] = (...args) => { COLLECT(method, ...args); original[method](...args) };
}
}());
// Python-like datetime.isoformat() method.
Date.prototype.isoformat = function ISODateString(sep, tz) {
if ( sep === undefined ) sep = 'T';
if ( tz === undefined ) tz = 'Z';
function pad(n) { return n < 10 ? '0' + n : n }
return this.getUTCFullYear() + '-'
+ pad(this.getUTCMonth() + 1) + '-'
+ pad(this.getUTCDate()) + sep
+ pad(this.getUTCHours()) + ':'
+ pad(this.getUTCMinutes()) + ':'
+ pad(this.getUTCSeconds()) + tz
}
window.console = {
original: console,
...console,
speak: (strings, func, start) => strings.forEach(string => {
speechSynthesis.speak(new SpeechSynthesisUtterance((start ? start : '') + string))
return console.original[func](string)
}),
log: (...strings) => console.speak(strings, 'log', `Dear diary. `),
info: (...strings) => console.speak(strings, 'info', `For your information. `),
warn: (...strings) => console.speak(strings, 'warn', `I'm warning you. `),
error: (...strings) => console.speak(strings, 'error', `Red alert. `)
}
// Semicolon use is roughshod due to lazy elimination of braces. Test it, throw out the ones that don't matter.
String.prototype.format = function () {
// Python str.format-like syntax interpolation. But, today, just use back-quoted string formatting. Examples:
// 'Lorem {0} ipsum.'.format('<script>') --> 'Lorem <script> ipsum.'
// 'Dolor {e0} amet.'.format('<script>') --> 'Dolor &lt;script&gt; amet.'
var args = arguments;
return this.replace(/\{(e?)(\d+)\}/g, function (match, flag, number) {
if (typeof args[number] != 'undefined') {
if (flag == 'e') return _.escape(args[number]);
else return args[number];
} else return match;
})
};
String.prototype.startsWith = function (str) {
return this.substr(0, str.length) === str
}
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1
}
if (!String.prototype.trim) {
String.prototype.ltrim = function() {
return this.replace(/^\s+/, '')
}
String.prototype.rtrim = function() {
return this.replace(/\s+$/, '')
}
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, '')
}
}
String.prototype.join = function (array, ...args) {
// Python-like ",".join([1, 2, 3]) or ",".join(1, 2, 3) for the lazy.
if ( args.length ) return [array, ...args].join(this)
else return array.join(this)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment