Last active
October 4, 2020 06:37
-
-
Save disco0/561c3b968cc08594b8b1f0444a39eb9a to your computer and use it in GitHub Desktop.
UserScript - Meta Parsing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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