Skip to content

Instantly share code, notes, and snippets.

@disco0
Last active October 4, 2020 06:37
Show Gist options
  • Save disco0/561c3b968cc08594b8b1f0444a39eb9a to your computer and use it in GitHub Desktop.
Save disco0/561c3b968cc08594b8b1f0444a39eb9a to your computer and use it in GitHub Desktop.
UserScript - Meta Parsing
'use strict';
const gAllMetaRegexp = new RegExp(
'^(\u00EF\u00BB\u00BF)?// ==UserScript==([\\s\\S]*?)^// ==/UserScript==',
'm');
/** Get just the stuff between ==UserScript== lines. */
function extractMeta(content)
{
const meta = content && content.match(gAllMetaRegexp);
return (meta) ? meta[2].replace(/^\s+/, '') : undefined
}
// Private implementation.
(function() {
/** Pull the filename part from the URL, without `.user.js`. */
function nameFromUrl(url: string)
{
let name = url.substring(0, url.indexOf(".user.js"));
name = name.substring(name.lastIndexOf("/") + 1);
return name;
}
// Safely construct a new URL object from a path and base. According to MDN,
// if a URL constructor received an absolute URL as the path then the base
// is ignored. Unfortunately that doesn't seem to be the case. And if the
// base is invalid (null / empty string) then an exception is thrown.
function safeUrl(path: string, base: string): URL
{
if (base)
return new URL(path, base);
else
return new URL(path);
}
// Defaults that can only be applied after the meta block has been parsed.
function prepDefaults(details) {
// We couldn't set this default above in case of real data, so if there's
// still no includes, set the default of include everything.
if (details.includes.length == 0 && details.matches.length == 0) {
details.includes.push('*');
}
if (details.grants.includes('none') && details.grants.length > 1) {
details.grants = ['none'];
}
return details;
}
/** Parse the source of a script; produce object of data. */
window.parseUserScript = function(content, url, failWhenMissing=false) {
if (!content) {
throw new Error('parseUserScript() got no content!');
}
// Populate with defaults in case the script specifies no value.
const details = {
'downloadUrl': url,
'excludes': [],
'grants': [],
'homePageUrl': null,
'includes': [],
'matches': [],
'name': url && nameFromUrl(url) || 'Unnamed Script',
'namespace': url && new URL(url).host || null,
'noFrames': false,
'requireUrls': [],
'resourceUrls': {},
'runAt': 'end'
};
let meta = extractMeta(content).match(/.+/g);
if (!meta) {
if (failWhenMissing) {
throw new Error('Could not parse, no meta.');
} else {
return prepDefaults(details);
}
}
let locales = {};
for (let i = 0, metaLine = ''; metaLine = meta[i]; i++) {
let data;
try {
data = parseMetaLine(metaLine.replace(/\s+$/, ''));
} catch (e) {
// Ignore invalid/unsupported meta lines.
continue;
}
switch (data.keyword) {
case 'noframes':
details.noFrames = true;
break;
case 'homepageURL':
details.homePageUrl = data.value;
break;
case 'namespace':
case 'version':
details[data.keyword] = data.value;
break;
case 'run-at':
details.runAt = data.value.replace('document-', '');
// TODO: Assert/normalize to supported value.
break;
case 'grant':
if (data.value == 'none' || SUPPORTED_APIS.has(data.value)) {
details.grants.push(data.value);
}
break;
case 'description':
case 'name':
let locale = data.locale;
if (locale) {
if (!locales[locale]) locales[locale] = {};
locales[locale][data.keyword] = data.value;
} else {
details[data.keyword] = data.value;
}
break;
case 'exclude':
details.excludes.push(data.value);
break;
case 'include':
details.includes.push(data.value);
break;
case 'match':
try {
new MatchPattern(data.value);
details.matches.push(data.value);
} catch (e) {
throw new Error(
_('ignoring_MATCH_because_REASON', data.value, e));
}
break;
case 'icon':
details.iconUrl = safeUrl(data.value, url).toString();
break;
case 'require':
details.requireUrls.push( safeUrl(data.value, url).toString() );
break;
case 'resource':
let resourceName = data.value1;
let resourceUrl = data.value2;
if (resourceName in details.resourceUrls) {
throw new Error(_('duplicate_resource_NAME', resourceName));
}
details.resourceUrls[resourceName] = safeUrl(resourceUrl, url).toString();
break;
}
}
return prepDefaults(details);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment