tern-browser-extension
Version:
A Tern plugin adding advanced features for Browser.
1,189 lines (1,112 loc) • 68.5 kB
JavaScript
(function (root, mod) {
if (typeof exports == 'object' && typeof module == 'object') // CommonJS
return mod(exports, require('tern/lib/infer'), require('tern/lib/tern'),
require('acorn'), require('sax'), require('csslint').CSSLint)
if (typeof define == 'function' && define.amd) // AMD
return define(['exports', 'tern/lib/infer', 'tern/lib/tern', 'acorn/dist/acorn', 'sax' ], mod)
mod(root, tern, tern, acorn, sax, root.CSSLint ? root.CSSLint : null)
})(this, function (exports, infer, tern, acorn, sax, CSSLint) {
'use strict'
var htmlExtensions = {
'htm': true,
'html': true,
'xhtml': true,
'jsp': true,
'jsf': true,
'php': true
}
var defaultRules = {
'UnknownElementId': {'severity': 'warning'},
'UnknownEventType': {'severity': 'warning'}
}
var startsWith = function (s, searchString, position) {
position = position || 0
return s.indexOf(searchString, position) === position
}
function resolvePath (from, to) {
if (to[0] === '/')
return to.slice(1)
var parts = from.split(/[\\/]/)
parts.pop()
parts = parts.concat(to.split(/[\\/]/))
for (var i = 1; i < parts.length; i++) {
if (parts[i] == '..') {
parts.splice(i - 1, 2)
i -= 2
}
}
return parts.join('/')
}
// DOM Document
function isScriptTag (tagName, scriptTags) {
for (var i = 0; i < scriptTags.length; i++) {
if (tagName.toLowerCase() == scriptTags[i]) return true
}
return false
}
function isScriptEvent (attrName) {
return startsWith(attrName, 'on')
}
function spaces (text, from, to) {
var spaces = ''
for (var i = from; i < to; i++) {
var c = text.charAt(i)
switch (c) {
case '\r':
case '\n':
case '\t':
spaces += c
break
default:
spaces += ' '
}
}
return spaces
}
var dummyAcornParser = {options: {}}
var DOMDocument = exports.DOMDocument = function (xml, file, scriptTags, server) {
var addFile = server !== undefined ? function (path, parentPath) {
if (!server._browserExtension.resolveFiles || !server._browserExtension.fileExists(path)) return
server.addFile(path, null, parentPath)
} : function(_, __) { }
if (!scriptTags) scriptTags = ['script']
var ids = this.ids = {}
var scripts = '', scriptParsing = false, from = 0, to = xml.length,
parser = sax.parser(true)
parser.onopentag = function (node) {
if (isScriptTag(node.name, scriptTags)) {
if (node.attributes.src != undefined) {
addFile(resolvePath(file.name, node.attributes.src), file.name)
return
}
scriptParsing = true
to = this.position
scripts = scripts + spaces(xml, from, to)
from = to
to = xml.length
} else if (node.name.toLowerCase() == 'link') {
if (node.attributes.rel == 'import' && node.attributes.href != undefined) {
addFile(resolvePath(file.name, node.attributes.href), file.name)
}
}
}
parser.onclosetag = function (tagName) {
if (isScriptTag(tagName, scriptTags)) {
scriptParsing = false
to = this.position
var endElement = '</' + tagName + '>'
scripts = scripts + xml.substring(from, to - endElement.length)
scripts = scripts + spaces(endElement, 0, endElement.length)
from = to
to = xml.length
}
}
parser.onattribute = function (attr) {
var startVal = this.position - attr.value.length - 1, endVal = this.position - 1
if (attr.name.toLowerCase() == 'id') {
var originNode = new acorn.Node(dummyAcornParser)
originNode.start = startVal
originNode.end = endVal
originNode.sourceFile = file
originNode.ownerElement = this.tagName
ids[attr.value] = originNode
} else if (isScriptEvent(attr.name)) {
scripts = scripts + spaces(xml, from, startVal)
scripts = scripts + attr.value
from = this.position - 1
to = xml.length
}
}
parser.write(xml)
if (from != to) {
if (scriptParsing) {
scripts = scripts + xml.substring(from, to)
} else {
scripts = scripts + spaces(xml, from, to)
}
}
this.scripts = scripts
}
function getHTMLElementName (tagName) {
return 'HTML' + tagName.substring(0, 1).toUpperCase() + tagName.substring(1, tagName.length) + 'Element'
}
// Custom tern function
function createElement (tagName) {
if (!tagName || tagName.length < 1) return new infer.Obj(infer.def.parsePath('HTMLElement.prototype'))
var cx = infer.cx(), server = cx.parent, name = getHTMLElementName(tagName),
locals = infer.def.parsePath(name + '.prototype')
if (locals && locals != infer.ANull) return new infer.Obj(locals)
return createElement()
}
infer.registerFunction('Browser_getElementById', function (_self, args, argNodes) {
if (!argNodes || !argNodes.length || argNodes[0].type != 'Literal' || typeof argNodes[0].value != 'string' || !(argNodes[0].sourceFile && argNodes[0].sourceFile.dom))
return createElement()
var cx = infer.cx(), id = argNodes[0].value, dom = argNodes[0].sourceFile.dom, attr = dom.ids[id]
argNodes[0].elementId = true
if (attr) {
argNodes[0].dom = attr
return createElement(attr.ownerElement)
}
return createElement()
})
infer.registerFunction('Browser_createElement', function (_self, args, argNodes) {
if (!argNodes || !argNodes.length || argNodes[0].type != 'Literal' || typeof argNodes[0].value != 'string')
return createElement()
return createElement(argNodes[0].value)
})
infer.registerFunction('Browser_querySelector', function (argN) {
return function (self, args, argNodes) {
if (!argNodes || !argNodes.length || argNodes[argN].type != 'Literal' || typeof argNodes[argN].value != 'string' || !(argNodes[argN].sourceFile && argNodes[argN].sourceFile.dom)) {
return
}
argNodes[argN].cssselectors = true
}
})
infer.registerFunction('Browser_eventType', function (argN) {
return function (self, args, argNodes) {
if (!argNodes || !argNodes.length || argNodes[argN].type != 'Literal' || typeof argNodes[argN].value != 'string') {
return
}
argNodes[argN].eventType = events[argNodes[argN].value]
argNodes[argN].eventTypable = true
}
})
// Plugin
tern.registerPlugin('browser-extension', function (server, options) {
server._browserExtension = {
scriptTags: (options && options.scriptTags) ? options.scriptTags : ['script'],
resolveFiles: options.resolveFiles || true,
fileExists: (typeof exports == 'object' && typeof module == 'object') ? require('fs').existsSync : function (_) { return true }
}
registerLints()
return {passes: {
preLoadDef: preLoadDef,
preParse: preParse,
typeAt: findTypeAt,
completion: findCompletions
}}
})
// Lint
function registerLints () {
if (!tern.registerLint) return
// validate document.addEventListener
tern.registerLint('Browser_validateEventType', function (node, addMessage, getRule) {
var argNode = node.arguments[0]
if (argNode && !argNode.eventType) {
addMessage(argNode, "Unknown event '" + argNode.value + "'", defaultRules.UnknownEventType.severity)
}
})
// validate document.getElementById
tern.registerLint('Browser_validateElementId', function (node, addMessage, getRule) {
var argNode = node.arguments[0]
if (argNode && !argNode.dom) {
addMessage(argNode, "Unknown element id '" + argNode.value + "'", defaultRules.UnknownElementId.severity)
}
})
// validate Element#querySelector/Element#queryAllSelector
tern.registerLint('Browser_validateCSSSelectors', function (node, addMessage, getRule) {
var argNode = node.arguments[0]
if (!argNode || argNode.type != 'Literal' || typeof argNode.value != 'string') {
return
}
if (!CSSLint) return
var rule = getRule('InvalidArgument')
if (!rule) return
var result = CSSLint.verify(argNode.value + '{}', { ids: 0, 'check-ids': 1})
if (result) {
if (result.messages.length > 0) {
// validate syntax of CSS selector
for (var i = 0; i < result.messages.length; i++) {
var message = result.messages[i], startCol = message.col - 1, endCol = message.col
var n = {start: argNode.start + startCol, end: argNode.start + endCol}
addMessage(n, "Invalid CSS selectors '" + argNode.value + "': " + message.message, rule.severity, startCol, endCol)
}
}
// validate existing of CSS#ID
var ids = result.stats['_elements']
if (ids) {
for (var i = 0; i < ids.length; i++) {
var modifier = ids[i], id = modifier.text, attr = getAttr(argNode, id.slice(1, id.length))
if (!attr) {
var n = {start: argNode.start + modifier.col, end: argNode.start + modifier.col + modifier.text.length}
addMessage(n, "Unknown element id '" + id + "'", defaultRules.UnknownElementId.severity)
}
}
}
}
})
if (CSSLint) {
CSSLint.addRule({
// rule information
id: 'check-ids',
name: 'Check existing of IDs in selectors',
desc: 'Check existing of IDs in selectors',
browsers: 'All',
// initialization
init: function (parser, reporter) {
var rule = this
parser.addListener('startrule', function (event) {
var selectors = event.selectors,
selector,
part,
modifier,
idCount,
i, j, k
for (i = 0; i < selectors.length; i++) {
selector = selectors[i]
idCount = 0
for (j = 0; j < selector.parts.length; j++) {
part = selector.parts[j]
if (part.type == parser.SELECTOR_PART_TYPE) {
for (k = 0; k < part.modifiers.length; k++) {
modifier = part.modifiers[k]
if (modifier.type == 'id') {
var id = modifier.text
var ids = reporter.stats['_elements']
if (!ids) {
ids = []
reporter.stat('_elements', ids)
}
ids.push(modifier)
}
}
}
}
}
})
}
})
}
}
// Pre load def : implementation to override getElementById an dcreateElement !type
function eventType(elt) {
elt['!effects'] = ['custom Browser_eventType 0']
elt['!data'] = {'!lint': 'Browser_validateEventType'}
}
function preLoadDef (data) {
var cx = infer.cx()
if (data['!name'] == 'browser') {
// Override Document#getElementById !type
var getElementById = data['Document']['prototype']['getElementById']
getElementById['!type'] = 'fn(id: string) -> !custom:Browser_getElementById'
getElementById['!data'] = {'!lint': 'Browser_validateElementId'}
// Override Document#createElement !type
var createElement = data['Document']['prototype']['createElement']
createElement['!type'] = 'fn(tagName: string) -> !custom:Browser_createElement'
// Add Element#querySelector !effects
var querySelector = data['Element']['prototype']['querySelector']
querySelector['!effects'] = ['custom Browser_querySelector 0']
querySelector['!data'] = {'!lint': 'Browser_validateCSSSelectors'}
// Add Element#querySelector !effects
var querySelectorAll = data['Element']['prototype']['querySelectorAll']
querySelectorAll['!effects'] = ['custom Browser_querySelector 0']
querySelectorAll['!data'] = {'!lint': 'Browser_validateCSSSelectors'}
// Add Document#addEventListener/removeListener !effects
eventType(data['addEventListener'])
eventType(data['removeEventListener'])
eventType(data['Node']['prototype']['addEventListener'])
eventType(data['Node']['prototype']['removeEventListener'])
eventType(data['Event']['prototype']['initEvent'])
}
}
// Pre parse
function isHTML (file) {
var name = file.name
if (name == '[doc]') return true
var index = name.lastIndexOf('.')
return index != -1 && htmlExtensions[name.substring(index + 1, name.length)] == true
}
function preParse (text, options) {
var file = options.directSourceFile
if (!isHTML(file)) return
var cx = infer.cx(), server = cx.parent, scriptTags = server._browserExtension.scriptTags
var dom = file.dom = new DOMDocument(text, file, scriptTags, server)
return dom.scripts
}
function getAttr (node, id) {
var dom = node.sourceFile.dom
return dom ? dom.ids[id] : null
}
// Find type at
function findTypeAt (_file, _pos, expr, type) {
if (!expr) return type
var isStringLiteral = expr.node.type === 'Literal' &&
typeof expr.node.value === 'string'
if (isStringLiteral) {
var attr = null
if (!!expr.node.dom) {
attr = expr.node.dom
} else if (!!expr.node.eventType) {
var eventType = expr.node.eventType
type = Object.create(type)
type.doc = eventType['!doc']
type.url = eventType['!url']
type.origin = "browser-extension"
} else if (expr.node.cssselectors == true) {
var text = _file.text, wordStart = _pos, wordEnd = _pos
while (wordStart && acorn.isIdentifierChar(text.charCodeAt(wordStart - 1))) --wordStart
while (wordEnd < text.length && acorn.isIdentifierChar(text.charCodeAt(wordEnd))) ++wordEnd
var id = text.slice(wordStart, wordEnd)
attr = getAttr(expr.node, id)
}
if (attr) {
// The `type` is a value shared for all string literals.
// We must create a copy before modifying `origin` and `originNode`.
// Otherwise all string literals would point to the last jump location
type = Object.create(type)
type.origin = attr.sourceFile.name
type.originNode = attr
}
}
return type
}
// Find completion
function findCompletions (file, query) {
var wordStart = tern.resolvePos(file, query.end), wordEnd = wordStart
var callExpr = infer.findExpressionAround(file.ast, null, wordStart, file.scope, 'CallExpression')
if (!callExpr) return
var callNode = callExpr.node
if (!callNode.callee || callNode.arguments.length < 1) return
var argNode = findAttrValue(callNode.arguments, wordStart), completionType = getCompletionType(argNode)
if (!completionType) return
var fileText = file.text, text = argNode.raw, quote = text.charAt(0)
if (!completionType.expr) {
wordStart = argNode.start + 1
wordEnd = argNode.end - 1
} else {
while (wordStart && acorn.isIdentifierChar(fileText.charCodeAt(wordStart - 1))) --wordStart
if (query.expandWordForward !== false)
while (wordEnd < fileText.length && acorn.isIdentifierChar(fileText.charCodeAt(wordEnd))) ++wordEnd
}
var word = fileText.slice(wordStart, wordEnd),
before = fileText.slice(argNode.start + 1, wordStart), after = fileText.slice(wordEnd, argNode.end - 1)
if (after && after.charAt(word.length - 1) == quote)
after = after.slice(0, word.length - 1)
var completions = []
completionType.complete(completions, query, file, word, wordStart)
if (argNode.end == wordEnd + 1 && file.text.charAt(wordEnd) == quote)
++wordEnd
return {
start: tern.outputPos(query, file, argNode.start),
end: tern.outputPos(query, file, argNode.end),
isProperty: false,
isObjectKey: false,
completions: completions.map(function (rec) {
var name = typeof rec == 'string' ? rec : rec.name
var string = JSON.stringify(before + name + after)
if (quote == "'") string = quote + string.slice(1, string.length - 1).replace(/'/g, "\\'") + quote
if (typeof rec == 'string') return string
if (!rec.displayName) rec.displayName = name
rec.name = string
return rec
})
}
}
function findAttrValue (argsNode, wordEnd) {
for (var i = 0; i < argsNode.length; i++) {
var argNode = argsNode[i]
if (argNode.type == 'Literal' && typeof argNode.value == 'string' &&
argNode.start < wordEnd && argNode.end > wordEnd) return argNode
}
}
function getCompletionType (argNode) {
if (!argNode) return
if (argNode.elementId) return {
complete: completeDOMElementIds,
expr: false
}
if (argNode.eventTypable) return {
complete: completeEventTypes,
expr: false
}
if (argNode.cssselectors) return {
complete: completeCSSSelectors,
expr: true
}
}
function completeElementIds (completions, query, file, word, withHash, addHash) {
var cx = infer.cx(), server = cx.parent, dom = file.dom, attrs = dom ? dom.ids : null
if (!attrs) return
var wrapAsObjs = query.types || query.depths || query.docs || query.urls || query.origins
function maybeSet (obj, prop, val) {
if (val != null) obj[prop] = val
}
function gather (attrs) {
for (var name in attrs) {
if (!name) continue
if (name &&
!(query.filter !== false && word &&
(query.caseInsensitive ? name.toLowerCase() : name).indexOf(word) !== 0)) {
var key = addHash ? '#' + name : name
var rec = wrapAsObjs ? {name: key} : key
completions.push(rec)
if (query.types || query.origins) {
if (query.types) rec.type = 'Attr'
if (query.origins)
maybeSet(rec, 'origin', file.name)
rec.displayName = withHash ? '#' + name : name
}
}
}
}
if (query.caseInsensitive) word = word.toLowerCase()
gather(attrs)
return completions
}
function completeHTMLElements (completions, query, file, word) {
var cx = infer.cx(), server = cx.parent, dom = file.dom, attrs = data
var wrapAsObjs = query.types || query.depths || query.docs || query.urls || query.origins
function maybeSet (obj, prop, val) {
if (val != null) obj[prop] = val
}
function gather (attrs) {
for (var name in attrs) {
if (!name) continue
if (name &&
!(query.filter !== false && word &&
(query.caseInsensitive ? name.toLowerCase() : name).indexOf(word) !== 0)) {
var rec = wrapAsObjs ? {name: name} : name
completions.push(rec)
if (query.types || query.origins) {
if (query.types) rec.type = getHTMLElementName(name)
if (query.origins)
maybeSet(rec, 'origin', file.name)
}
}
}
}
if (query.caseInsensitive) word = word.toLowerCase()
gather(data)
return completions
}
function completeSelectors (completions, query, file, word) {
var cx = infer.cx(), server = cx.parent, dom = file.dom
var wrapAsObjs = query.types || query.depths || query.docs || query.urls || query.origins
function maybeSet (obj, prop, val) {
if (val != null) obj[prop] = val
}
function gather (attrs) {
for (var name in attrs) {
if (!name) continue
if (name &&
!(query.filter !== false && word &&
(query.caseInsensitive ? name.toLowerCase() : name).indexOf(word) !== 0)) {
var rec = wrapAsObjs ? {name: name} : name
completions.push(rec)
if (query.types || query.origins || query.urls || query.docs) {
// if (query.types) rec.type = getHTMLElementName(name)
if (query.origins)
maybeSet(rec, 'origin', file.name)
if (query.docs && attrs[name]['!doc']) rec.doc = attrs[name]['!doc']
if (query.urls && attrs[name]['!url']) rec.url = attrs[name]['!url']
}
}
}
}
if (query.caseInsensitive) word = word.toLowerCase()
gather(selectors)
return completions
}
function completeDOMElementIds (completions, query, file, word) {
completeElementIds(completions, query, file, word)
}
function completeCSSSelectors (completions, query, file, word, wordStart) {
var hasHash = file.text.charAt(wordStart - 1) == '#'
completeElementIds(completions, query, file, word, true, !hasHash)
if (!hasHash) {
completeHTMLElements(completions, query, file, word)
completeSelectors(completions, query, file, word)
}
}
function completeEventTypes (completions, query, file, word) {
var cx = infer.cx(), server = cx.parent, dom = file.dom, attrs = data
var wrapAsObjs = query.types || query.depths || query.docs || query.urls || query.origins
function maybeSet (obj, prop, val) {
if (val != null) obj[prop] = val
}
function gather (attrs) {
for (var name in attrs) {
if (!name) continue
if (name &&
!(query.filter !== false && word &&
(query.caseInsensitive ? name.toLowerCase() : name).indexOf(word) !== 0)) {
var rec = wrapAsObjs ? {name: name} : name
completions.push(rec)
if (query.types || query.origins) {
if (query.types) rec.type = "string"
if (query.docs && attrs[name]['!doc']) rec.doc = attrs[name]['!doc']
if (query.urls && attrs[name]['!url']) rec.url = attrs[name]['!url']
if (query.origins) rec.origin = "browser-extension"
}
}
}
}
if (query.caseInsensitive) word = word.toLowerCase()
gather(events)
return completions
}
var langs = 'ab aa af ak sq am ar an hy as av ae ay az bm ba eu be bn bh bi bs br bg my ca ch ce ny zh cv kw co cr hr cs da dv nl dz en eo et ee fo fj fi fr ff gl ka de el gn gu ht ha he hz hi ho hu ia id ie ga ig ik io is it iu ja jv kl kn kr ks kk km ki rw ky kv kg ko ku kj la lb lg li ln lo lt lu lv gv mk mg ms ml mt mi mr mh mn na nv nb nd ne ng nn no ii nr oc oj cu om or os pa pi fa pl ps pt qu rm rn ro ru sa sc sd se sm sg sr gd sn si sk sl so st es su sw ss sv ta te tg th ti bo tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa cy wo fy xh yi yo za zu'.split(' ')
var targets = ['_blank', '_self', '_top', '_parent']
var charsets = ['ascii', 'utf-8', 'utf-16', 'latin1', 'latin1']
var methods = ['get', 'post', 'put', 'delete']
var encs = ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']
var media = ['all', 'screen', 'print', 'embossed', 'braille', 'handheld', 'print', 'projection', 'screen', 'tty', 'tv', 'speech',
'3d-glasses', 'resolution [>][<][=] [X]', 'device-aspect-ratio: X/Y', 'orientation:portrait',
'orientation:landscape', 'device-height: [X]', 'device-width: [X]']
var s = { attrs: {} }; // Simple tag, reused for a whole lot of tags
var data = {
a: {
attrs: {
href: null, ping: null, type: null,
media: media,
target: targets,
hreflang: langs
}
},
abbr: s,
acronym: s,
address: s,
applet: s,
area: {
attrs: {
alt: null, coords: null, href: null, target: null, ping: null,
media: media, hreflang: langs, type: null,
shape: ['default', 'rect', 'circle', 'poly']
}
},
article: s,
aside: s,
audio: {
attrs: {
src: null, mediagroup: null,
crossorigin: ['anonymous', 'use-credentials'],
preload: ['none', 'metadata', 'auto'],
autoplay: ['', 'autoplay'],
loop: ['', 'loop'],
controls: ['', 'controls']
}
},
b: s,
base: { attrs: { href: null, target: targets } },
basefont: s,
bdi: s,
bdo: s,
big: s,
blockquote: { attrs: { cite: null } },
body: s,
br: s,
button: {
attrs: {
form: null, formaction: null, name: null, value: null,
autofocus: ['', 'autofocus'],
disabled: ['', 'autofocus'],
formenctype: encs,
formmethod: methods,
formnovalidate: ['', 'novalidate'],
formtarget: targets,
type: ['submit', 'reset', 'button']
}
},
canvas: { attrs: { width: null, height: null } },
caption: s,
center: s,
cite: s,
code: s,
col: { attrs: { span: null } },
colgroup: { attrs: { span: null } },
command: {
attrs: {
type: ['command', 'checkbox', 'radio'],
label: null, icon: null, radiogroup: null, command: null, title: null,
disabled: ['', 'disabled'],
checked: ['', 'checked']
}
},
data: { attrs: { value: null } },
datagrid: { attrs: { disabled: ['', 'disabled'], multiple: ['', 'multiple'] } },
datalist: { attrs: { data: null } },
dd: s,
del: { attrs: { cite: null, datetime: null } },
details: { attrs: { open: ['', 'open'] } },
dfn: s,
dir: s,
div: s,
dl: s,
dt: s,
em: s,
embed: { attrs: { src: null, type: null, width: null, height: null } },
eventsource: { attrs: { src: null } },
fieldset: { attrs: { disabled: ['', 'disabled'], form: null, name: null } },
figcaption: s,
figure: s,
font: s,
footer: s,
form: {
attrs: {
action: null, name: null,
'accept-charset': charsets,
autocomplete: ['on', 'off'],
enctype: encs,
method: methods,
novalidate: ['', 'novalidate'],
target: targets
}
},
frame: s,
frameset: s,
h1: s, h2: s, h3: s, h4: s, h5: s, h6: s,
head: {
attrs: {},
children: ['title', 'base', 'link', 'style', 'meta', 'script', 'noscript', 'command']
},
header: s,
hgroup: s,
hr: s,
html: {
attrs: { manifest: null },
children: ['head', 'body']
},
i: s,
iframe: {
attrs: {
src: null, srcdoc: null, name: null, width: null, height: null,
sandbox: ['allow-top-navigation', 'allow-same-origin', 'allow-forms', 'allow-scripts'],
seamless: ['', 'seamless']
}
},
img: {
attrs: {
alt: null, src: null, ismap: null, usemap: null, width: null, height: null,
crossorigin: ['anonymous', 'use-credentials']
}
},
input: {
attrs: {
alt: null, dirname: null, form: null, formaction: null,
height: null, list: null, max: null, maxlength: null, min: null,
name: null, pattern: null, placeholder: null, size: null, src: null,
step: null, value: null, width: null,
accept: ['audio/*', 'video/*', 'image/*'],
autocomplete: ['on', 'off'],
autofocus: ['', 'autofocus'],
checked: ['', 'checked'],
disabled: ['', 'disabled'],
formenctype: encs,
formmethod: methods,
formnovalidate: ['', 'novalidate'],
formtarget: targets,
multiple: ['', 'multiple'],
readonly: ['', 'readonly'],
required: ['', 'required'],
type: ['hidden', 'text', 'search', 'tel', 'url', 'email', 'password', 'datetime', 'date', 'month',
'week', 'time', 'datetime-local', 'number', 'range', 'color', 'checkbox', 'radio',
'file', 'submit', 'image', 'reset', 'button']
}
},
ins: { attrs: { cite: null, datetime: null } },
kbd: s,
keygen: {
attrs: {
challenge: null, form: null, name: null,
autofocus: ['', 'autofocus'],
disabled: ['', 'disabled'],
keytype: ['RSA']
}
},
label: { attrs: { 'for': null, form: null } },
legend: s,
li: { attrs: { value: null } },
link: {
attrs: {
href: null, type: null,
hreflang: langs,
media: media,
sizes: ['all', '16x16', '16x16 32x32', '16x16 32x32 64x64']
}
},
map: { attrs: { name: null } },
mark: s,
menu: { attrs: { label: null, type: ['list', 'context', 'toolbar'] } },
meta: {
attrs: {
content: null,
charset: charsets,
name: ['viewport', 'application-name', 'author', 'description', 'generator', 'keywords'],
'http-equiv': ['content-language', 'content-type', 'default-style', 'refresh']
}
},
meter: { attrs: { value: null, min: null, low: null, high: null, max: null, optimum: null } },
nav: s,
noframes: s,
noscript: s,
object: {
attrs: {
data: null, type: null, name: null, usemap: null, form: null, width: null, height: null,
typemustmatch: ['', 'typemustmatch']
}
},
ol: { attrs: { reversed: ['', 'reversed'], start: null, type: ['1', 'a', 'A', 'i', 'I'] } },
optgroup: { attrs: { disabled: ['', 'disabled'], label: null } },
option: { attrs: { disabled: ['', 'disabled'], label: null, selected: ['', 'selected'], value: null } },
output: { attrs: { 'for': null, form: null, name: null } },
p: s,
param: { attrs: { name: null, value: null } },
pre: s,
progress: { attrs: { value: null, max: null } },
q: { attrs: { cite: null } },
rp: s,
rt: s,
ruby: s,
s: s,
samp: s,
script: {
attrs: {
type: ['text/javascript'],
src: null,
async: ['', 'async'],
defer: ['', 'defer'],
charset: charsets
}
},
section: s,
select: {
attrs: {
form: null, name: null, size: null,
autofocus: ['', 'autofocus'],
disabled: ['', 'disabled'],
multiple: ['', 'multiple']
}
},
small: s,
source: { attrs: { src: null, type: null, media: null } },
span: s,
strike: s,
strong: s,
style: {
attrs: {
type: ['text/css'],
media: media,
scoped: null
}
},
sub: s,
summary: s,
sup: s,
table: s,
tbody: s,
td: { attrs: { colspan: null, rowspan: null, headers: null } },
textarea: {
attrs: {
dirname: null, form: null, maxlength: null, name: null, placeholder: null,
rows: null, cols: null,
autofocus: ['', 'autofocus'],
disabled: ['', 'disabled'],
readonly: ['', 'readonly'],
required: ['', 'required'],
wrap: ['soft', 'hard']
}
},
tfoot: s,
th: { attrs: { colspan: null, rowspan: null, headers: null, scope: ['row', 'col', 'rowgroup', 'colgroup'] } },
thead: s,
time: { attrs: { datetime: null } },
title: s,
tr: s,
track: {
attrs: {
src: null, label: null, 'default': null,
kind: ['subtitles', 'captions', 'descriptions', 'chapters', 'metadata'],
srclang: langs
}
},
tt: s,
u: s,
ul: s,
'var': s,
video: {
attrs: {
src: null, poster: null, width: null, height: null,
crossorigin: ['anonymous', 'use-credentials'],
preload: ['auto', 'metadata', 'none'],
autoplay: ['', 'autoplay'],
mediagroup: ['movie'],
muted: ['', 'muted'],
controls: ['', 'controls']
}
},
wbr: s
}
var globalAttrs = {
accesskey: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'class': null,
contenteditable: ['true', 'false'],
contextmenu: null,
dir: ['ltr', 'rtl', 'auto'],
draggable: ['true', 'false', 'auto'],
dropzone: ['copy', 'move', 'link', 'string:', 'file:'],
hidden: ['hidden'],
id: null,
inert: ['inert'],
itemid: null,
itemprop: null,
itemref: null,
itemscope: ['itemscope'],
itemtype: null,
lang: ['en', 'es'],
spellcheck: ['true', 'false'],
style: null,
tabindex: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
title: null,
translate: ['yes', 'no'],
onclick: null,
rel: ['stylesheet', 'alternate', 'author', 'bookmark', 'help', 'license', 'next', 'nofollow', 'noreferrer', 'prefetch', 'prev', 'search', 'tag']
}
function populate (obj) {
for (var attr in globalAttrs) if (globalAttrs.hasOwnProperty(attr))
obj.attrs[attr] = globalAttrs[attr]
}
populate(s)
for (var tag in data) if (data.hasOwnProperty(tag) && data[tag] != s)
populate(data[tag])
var selectors = {
'*': {
'!doc': 'The universal selector, written as a CSS qualified name [CSS3NAMESPACE] with an asterisk (* U+002A) as the local name, represents the qualified name of any element type.',
'!url': 'http://www.w3.org/TR/css3-selectors/#universal-selector'
},
':link': {
'!doc': 'The :link pseudo-class applies to links that have not yet been visited.',
'!url': 'http://www.w3.org/TR/css3-selectors/#the-link-pseudo-classes-link-and-visited'
},
':visited': {
'!doc': 'The :visited pseudo-class applies once the link has been visited by the user.',
'!url': 'http://www.w3.org/TR/css3-selectors/#the-link-pseudo-classes-link-and-visited'
},
':hover': {
'!doc': 'The :hover pseudo-class applies while the user designates an element with a pointing device, but does not necessarily activate it. For example, a visual user agent could apply this pseudo-class when the cursor (mouse pointer) hovers over a box generated by the element. User agents not that do not support interactive media do not have to support this pseudo-class. Some conforming user agents that support interactive media may not be able to support this pseudo-class (e.g., a pen device that does not detect hovering).',
'!url': 'http://www.w3.org/TR/css3-selectors/#the-user-action-pseudo-classes-hover-act'
},
':active': {
'!doc': "The :active pseudo-class applies while an element is being activated by the user. For example, between the times the user presses the mouse button and releases it. On systems with more than one mouse button, :active applies only to the primary or primary activation button (typically the 'left' mouse button), and any aliases thereof.",
'!url': 'http://www.w3.org/TR/css3-selectors/#the-user-action-pseudo-classes-hover-act'
},
':focus': {
'!doc': 'The :focus pseudo-class applies while an element has the focus (accepts keyboard or mouse events, or other forms of input).',
'!url': 'http://www.w3.org/TR/css3-selectors/#the-user-action-pseudo-classes-hover-act'
}
}
// See https://developer.mozilla.org/docs/Web/Events
var events = {
'abort': {
'!doc': 'The abort event is fired when the loading of a resource has been aborted.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/abort'
},
'afterprint': {
'!doc': 'The afterprint event is fired after the associated document has started printing or the print preview has been closed.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/afterprint'
},
'animationend': {
'!doc': 'The animationend event is fired when a CSS animation has completed.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/animationend'
},
'animationiteration': {
'!doc': 'The animationend event is fired when an iteration of an animation ends. This event does not occur for animations with an animation-iteration-count of one.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/animationiteration'
},
'animationstart': {
'!doc': 'The animationstart event is fired when a CSS animation has started. If there is an animation-delay then this event will fire once the delay period has expired. A negative delay will cause the event to fire with an elapsedTime equal to the absolute value of the delay.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/animationstart'
},
'audioprocess': {
'!doc': 'The audioprocess event is fired when an input buffer of a Web Audio API ScriptProcessorNode is ready to be processed.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/audioprocess'
},
'beforeprint': {
'!doc': 'The beforeprint event is fired when the associated document is about to be printed or previewed for printing.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/beforeprint'
},
'beforeunload': {
'!doc': 'The beforeunload event is fired when the window, the document and its resources are about to be unloaded.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload'
},
'beginEvent': {
'!doc': 'The beginEvent event is fired when the element local timeline begins to play. It will be raised each time the element begins the active duration (i.e., when it restarts, but not when it repeats). It may be raised both in the course of normal (i.e. scheduled or interactive) timeline play, as well as in the case that the element was begun with a DOM method.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/beginEvent'
},
'blocked': {
'!doc': 'The blocked handler is executed when an open connection to a database is blocking a versionchange transaction on the same database.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/blocked'
},
'blur': {
'!doc': 'The blur event is fired when an element has lost focus. The main difference between this event and focusout is that only the latter bubbles.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/blur'
},
'cached': {
'!doc': 'The cached event is fired when the resources listed in the application cache manifest have been downloaded, and the application is now cached.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/cached'
},
'canplay': {
'!doc': 'The canplay event is fired when the user agent can play the media, but estimates that not enough data has been loaded to play the media up to its end without having to stop for further buffering of content.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/canplay'
},
'canplaythrough': {
'!doc': 'The canplaythrough event is fired when the user agent can play the media, and estimates that enough data has been loaded to play the media up to its end without having to stop for further buffering of content.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/canplaythrough'
},
'change': {
'!doc': "The change event is fired for <input>, <select>, and <textarea> elements when a change to the element's value is committed by the user. Unlike the input event, the change event is not necessarily fired for each change to an element's value.",
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/change'
},
'chargingchange': {
'!doc': 'The chargingchange event is fired when the charging attribute of the battery API has changed.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/chargingchange'
},
'chargingtimechange': {
'!doc': 'The chargingtimechange event is fired when the chargingTime attribute of the battery API has changed.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/chargingtimechange'
},
'checking': {
'!doc': 'The checking event is fired when the user agent is checking for an update, or attempting to download the cache manifest for the first time.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/checking'
},
'click': {
'!doc': 'The click event is fired when a pointing device button (usually a mouse button) is pressed and released on a single element.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/click'
},
'close': {
'!doc': 'The close handler is executed when a connection with a websocket is closed.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/close'
},
'complete': {
'!doc': 'The complete event is fired when the rendering of an OfflineAudioContext is terminated.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/complete'
},
'compositionend': {
'!doc': 'The compositionend event is fired when the composition of a passage of text has been completed or cancelled (fires with special characters that require a sequence of keys and other inputs such as speech recognition or word suggestion on mobile).',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/compositionend'
},
'compositionstart': {
'!doc': 'The compositionstart event is fired when the composition of a passage of text is prepared (similar to keydown for a keyboard input, but fires with special characters that require a sequence of keys and other inputs such as speech recognition or word suggestion on mobile).',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/compositionstart'
},
'compositionupdate': {
'!doc': 'The compositionupdate event is fired when a character is added to a passage of text being composed (fires with special characters that require a sequence of keys and other inputs such as speech recognition or word suggestion on mobile).',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/compositionupdate'
},
'contextmenu': {
'!doc': 'The contextmenu event is fired when the right button of the mouse is clicked (before the context menu is displayed), or when the context menu key is pressed (in which case the context menu is displayed at the bottom left of the focused element, unless the element is a tree, in which case the context menu is displayed at the bottom left of the current row).',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/contextmenu'
},
'copy': {
'!doc': 'The copy event is fired when a selection has been added to the clipboard.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/copy'
},
'cut': {
'!doc': 'The cut event is fired when a selection has been removed from the document and added to the clipboard.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/cut'
},
'dblclick': {
'!doc': 'The dblclick event is fired when a pointing device button (usually a mouse button) is clicked twice on a single element.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/dblclick'
},
'devicelight': {
'!doc': 'The devicelight event is fired when fresh data is available from a light sensor.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/devicelight'
},
'devicemotion': {
'!doc': 'The devicemotion event is fired at a regular interval and indicates the amount of physical force of acceleration the device is receiving at that time. It also provides information about the rate of rotation, if available.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/devicemotion'
},
'deviceorientation': {
'!doc': 'The deviceorientation event is fired when fresh data is available from an orientation sensor about the current orientation of the device as compared to the Earth coordinate frame. This data is gathered from a magnetometer inside the device.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/deviceorientation'
},
'deviceproximity': {
'!doc': 'The deviceproximity event is fired when fresh data is available from a proximity sensor.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/deviceproximity'
},
'dischargingtimechange': {
'!doc': 'The dischargingtimechange event is fired when the dischargingTime attribute of the battery API has changed.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/dischargingtimechange'
},
'DOMContentLoaded': {
'!doc': 'The DOMContentLoaded event is fired when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. A very different event - load - should be used only to detect a fully-loaded page. It is an incredibly popular mistake for people to use load where DOMContentLoaded would be much more appropriate, so be cautious.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded'
},
'downloading': {
'!doc': 'The downloading event is fired after checking for an application cache update, if the user agent has found an update and is fetching it, or is downloading the resources listed by the cache manifest for the first time.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/downloading'
},
'drag': {
'!doc': 'The drag event is fired when an element or text selection is being dragged (every few hundred milliseconds).',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/drag'
},
'dragend': {
'!doc': 'The dragend event is fired when a drag operation is being ended (by releasing a mouse button or hitting the escape key).',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/dragend'
},
'dragenter': {
'!doc': 'The dragenter event is fired when a dragged element or text selection enters a valid drop target.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/dragenter'
},
'dragleave': {
'!doc': 'The dragleave event is fired when a dragged element or text selection leaves a valid drop target.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/dragleave'
},
'dragover': {
'!doc': 'The dragover event is fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds).',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/dragover'
},
'dragstart': {
'!doc': 'The dragstart event is fired when the user starts dragging an element or text selection.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/dragstart'
},
'drop': {
'!doc': 'The drop event is fired when an element or text selection is dropped on a valid drop target.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/drop'
},
'durationchange': {
'!doc': 'The durationchange event is fired when the duration attribute has been updated.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/durationchange'
},
'emptied': {
'!doc': 'The emptied event is fired when the media has become empty; for example, this event is sent if the media has already been loaded (or partially loaded), and the load() method is called to reload it.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/emptied'
},
'ended': {
'!doc': 'The ended event is fired when playback has stopped because the end of the media was reached.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/ended'
},
'endEvent': {
'!doc': 'The endEvent event is fired when at the active end of the element is reached. Note that this event is not raised at the simple end of each repeat. This event may be raised both in the course of normal (i.e. scheduled or interactive) timeline play, as well as in the case that the element was ended with a DOM method.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/endEvent'
},
'error': {
'!doc': 'The error event is fired when a resource failed to load.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/error'
},
'focus': {
'!doc': 'The focus event is fired when an element has received focus. The main difference between this event and focusin is that only the latter bubbles.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/focus'
},
'fullscreenchange': {
'!doc': 'The fullscreenchange event is fired when the browser is switched to/out-of fullscreen mode.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/fullscreenchange'
},
'fullscreenerror': {
'!doc': 'The fullscreenerror event is fired when the browser cannot switch to fullscreen mode.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/fullscreenerror'
},
'gampadconnected': {
'!doc': 'The gampadconnected event is fired when the browser detects that a gamepad has been connected or the first time a buttuon/axis of the gamepad is used.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/gampadconnected'
},
'gampaddisconnected': {
'!doc': 'The gampaddisconnected event is fired when the browser detects that a gamepad has been disconnected.',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/gampaddisconnected'
},
'hashchange': {
'!doc': 'The hashchange event is fired when the fragment identifier of the URL has changed (the part of the URL that follows the # symbol, including the # symbol).',
'!url': 'https://developer.mozilla.org/en-US/docs/Web/Events/hashchange'
},
'input': {
'!doc': "The DOM input event is fired synchronously when the value of an <input> or <textarea> element is changed. Additionally, it fires on contenteditable editors when its contents are changed. In this case, the event target is the editing host element. If there are two or more elements which have contenteditable as true, 'editing host' is the nearest ancestor element whose parent isn't editable. Similarly, it's also fired on root element of