ESLint plugin to validate JavaScript and CSS usage against the legacy HbbTV/OIPF compatibility profile.
The allow-lists are derived directly from the OIPF DAE Reference Guide, keeping only the elements that document marks as supported. You can override any of them to match a specific device profile.
Reference documents:
- OIPF DAE Reference Guide v1.0 (2010-03-11) — the per-API support tables
- OIPF Volume 5 DAE V1.1 (2009-10-08) — normative; section 1 of the Reference Guide states that Volume 5 V1.1 takes precedence on any discrepancy
- OIPF Volume 5 DAE V1.0 (2009-01-06)
The allow-lists target Volume 5 V1.1. Where its Annex B contradicts the Reference Guide's tables, Annex B wins — see Known spec discrepancies.
V1.0 is stricter than V1.1: it does not add window.self, window.removeEventListener,
window.setInterval, window.postMessage, KeyEvent.charCode or the Navigator property, and it
profiles DOM Level 2 HTML as an explicit include-list rather than "all except three interfaces". To
target a V1.0 device, narrow the lists with the supportedGlobals and supportedMembers options.
Requires ESLint 9 or 10 and flat config. Node ^20.19.0 || ^22.13.0 || >=24.
eslint.config.js:
const hbbtvCompat = require("eslint-plugin-hbbtv-compat");
module.exports = [
hbbtvCompat.configs.recommended,
hbbtvCompat.configs.html,
hbbtvCompat.configs.css
];configs.recommended enables all three rules for JavaScript. configs.html registers the HTML
processor for **/*.html and **/*.htm and runs all three rules inside it: the processor extracts
both <style> blocks (linted by the CSS rule) and inline <script> bodies (linted by the two
JavaScript rules). configs.css registers the stylesheet processor for **/*.css and runs the CSS
rule over whole stylesheets.
ESLint gives a file exactly one processor, and eslint-plugin-html does not declare itself through
processor: — it patches Linter and bows out silently as soon as an explicit processor is set for
the file. Setting processor: "hbbtv-compat/html" alongside it therefore raises no error; it just
stops inline JavaScript from being linted by that plugin. Since the HTML processor here now covers
<script> as well as <style>, drop eslint-plugin-html for HbbTV projects rather than running two
configs and two eslint invocations.
A <script> element is skipped when it has a src attribute, is self-closing, has a whitespace-only
body, or carries a type that is not JavaScript. Absent type, text/javascript,
application/javascript, application/ecmascript and text/ecmascript count as JavaScript;
type="module" does not, since an ES module cannot run on an edition 3 engine at all.
Inline <script> supports --fix: fix ranges from script blocks are translated back onto the
document. Fixes reported inside a <style> block are dropped instead — that block is a synthesized
string literal, so no offset in it maps onto the HTML.
Turn prettier/prettier off for *.html and *.css, the same way configs.css consumers
already do. The rule sees an indented fragment stripped of its surrounding HTML and tries to reformat
it back into a shape that does not fit the document.
{
files: ["**/*.html", "**/*.htm", "**/*.css"],
rules: { "prettier/prettier": "off" }
}Out of scope, by design:
on*handler attributes (onclick="…") — the attribute value is never extracted- inline
styleattributes — seeno-unsupported-css - Vue, Svelte and other single-file component formats
- elements found by the regex scan inside HTML comments, CDATA sections or attribute values, and
start tags whose attribute value contains
>— the document is scanned, not parsed
Two optional presets enable ESLint core rules that matter for this profile but that this plugin's
own rules cannot see. They add no rules of their own — the checks are already implemented by
well-tested core rules, and the presets exist only so you do not hand-maintain the same rules block in
every project. Both are standalone and do not assume js/recommended is also extended.
module.exports = [
hbbtvCompat.configs.recommended,
hbbtvCompat.configs.legacyRuntime,
hbbtvCompat.configs.styleOpinions, // optional
hbbtvCompat.configs.html,
hbbtvCompat.configs.css
];configs.legacyRuntime — 10 core rules with a rationale in the specification or in the behaviour
of the ECMA-262 edition 3 runtimes it profiles:
| Rule | Why |
|---|---|
no-implied-eval |
Volume 5 V1.1 Annex B types setTimeout/setInterval as (Function f, Number d); the string form is off-spec |
no-new-func |
a Function body passed as a string is never parsed as source, so it slips past no-post-es3-syntax and past an ecmaVersion: 3 parser alike |
no-proto, no-iterator |
__proto__ and __iterator__ are legacy engine extensions, in neither edition 3 nor any DAE table |
guard-for-in, no-extend-native |
section 3.1 exposes no Object.keys, so for...in is the only enumeration idiom and inherited properties are a live hazard |
no-prototype-builtins |
section 3.1 lists Object.prototype.hasOwnProperty, but an edition 3 object can shadow it |
block-scoped-var |
edition 3 has no let/const, making var hoisting the dominant scoping bug |
radix |
edition 3 permits parseInt to read a leading zero as octal |
no-with |
legal in edition 3, but defeats scope analysis for every other rule |
no-proto and no-iterator are the clearest case for this preset: no-unsupported-js carries no
type inference, so it never checks members on local variables and cannot catch them.
configs.styleOpinions — 19 general code-quality core rules (eqeqeq, curly, default-case,
no-labels, yoda, no-param-reassign, …). These have no basis in either specification
document and are bundled purely as a convenience. Skip this preset if you already have a house
style; it is deliberately separate from legacyRuntime for that reason.
Neither preset includes no-alert (no-unsupported-js already reports alert, confirm, prompt
and window.alert) or no-extra-label (no-labels already forbids every label).
To wire things up manually:
const hbbtvCompat = require("eslint-plugin-hbbtv-compat");
module.exports = [
{
plugins: { "hbbtv-compat": hbbtvCompat },
rules: {
"hbbtv-compat/no-unsupported-js": "error",
"hbbtv-compat/no-unsupported-css": "error",
"hbbtv-compat/no-post-es3-syntax": "error"
}
},
{
files: ["**/*.html"],
plugins: { "hbbtv-compat": hbbtvCompat },
processor: "hbbtv-compat/html",
rules: {
"hbbtv-compat/no-unsupported-css": "error",
"hbbtv-compat/no-unsupported-js": "error",
"hbbtv-compat/no-post-es3-syntax": "error"
}
},
{
files: ["**/*.css"],
plugins: { "hbbtv-compat": hbbtvCompat },
processor: "hbbtv-compat/css",
rules: { "hbbtv-compat/no-unsupported-css": "error" }
}
];Both processors hand their CSS to the rule inside a generated JavaScript block. Inline <script>
bodies are handed over as-is, without a wrapper. Every block — generated or raw — inherits its
rules and its languageOptions from whichever config matched the source file. You cannot
target them with a config block of your own, because a glob such as **/*.css/*.js does not match a
block.
That inheritance is the constraint worth knowing about. In an HbbTV project the config block covering
**/*.html and **/*.css normally sets ecmaVersion: 3, and the generated block is then parsed as
edition 3 too. The generated code is therefore written as edition 3 — var plus a plain string
literal — so it parses. Emitting a template literal instead costs you the CSS findings for that file
and replaces them with a parse error on generated code you never wrote.
It applies to inline <script> the same way: with ecmaVersion: 3, post-edition-3 syntax in an
inline script is a parse error rather than a no-post-es3-syntax report, and ESLint then drops
every other finding for the whole document. If the inline scripts in your HTML are not edition 3 yet,
leave the HTML config block on a newer ecmaVersion and let no-post-es3-syntax report the syntax
instead.
The processor keeps blocks parseable where it can. HbbTV pages are XHTML, so inline scripts are
commonly wrapped in //<![CDATA[ … //]]>, <![CDATA[ … ]]> or a legacy <!-- … //--> comment. Those
markers are replaced with an equal number of spaces, so they cannot cause a parse error while every
line, column and fix offset in the block still matches the document.
Reports globals and members outside the profile, based on sections 3.1–3.10 (ECMA-262 edition 3
basic objects, DOM0, DOM Level 2 Core/Events/Style/HTML/View, XMLHttpRequest) and 4.1
(OIPF JavaScript APIs).
Catches, for example, Promise, fetch, JSON, localStorage, new Image(), Object.keys,
Array.from, [].indexOf, "x".trim(), fn.bind(), window.alert, window.removeEventListener
(section 3.2 marks it unsupported on Window specifically), navigator.userAgent,
document.querySelector and document.createElementNS.
Because the rule does no type inference, members are only checked on supported globals, on
X.prototype, and on array/string literals. Members of local variables holding DOM elements or
XMLHttpRequest instances are not checked.
{
"hbbtv-compat/no-unsupported-js": [
"error",
{
"additionalSupportedGlobals": ["LibShim", "VendorBridge"],
"supportedGlobals": ["window", "document", "Object", "Array"],
"supportedMembers": {
"Object": ["keys"],
"Array.prototype": ["indexOf", "join"],
"String.prototype": ["indexOf", "substring"]
},
"additionalSupportedMembers": {
"window": ["stop"]
}
}
]
}supportedGlobals and supportedMembers replace the built-in allow-lists.
additionalSupportedGlobals extends the default global allow-list, which is the right knob for
globals exported by external libraries.
additionalSupportedMembers extends the member allow-list (the built-in one, or supportedMembers
when set) per owner, which is the right knob for a single member the target devices are known to
support despite the spec, such as window.stop.
Section 3.1 scopes JavaScript support to ECMA-262 edition 3. Newer syntax fails to parse on
the device rather than failing at runtime, so it cannot be shimmed the way a missing API can.
Reports let/const, arrow functions, classes, template literals, for...of, spread/rest,
destructuring, default parameters, generators, async/await, getters/setters, shorthand and
computed properties, object method shorthand, object-literal and argument-list trailing commas,
reserved words as unquoted property names or identifiers, import/export, optional chaining,
??, logical assignment, **, BigInt literals, numeric separators, catch without a binding,
new.target, and regular expression flags beyond g, i and m.
Trailing commas in array literals are not reported: ECMA-262 edition 3 already allowed a trailing
Elision there.
{
"hbbtv-compat/no-post-es3-syntax": [
"error",
{ "allow": ["block-scoped-declaration", "template-literal"] }
]
}allow opts individual features out by key, for code that is transpiled before shipping. Keys are
the feature names used in the rule's messages, kebab-cased — see FEATURES in
lib/rules/no-post-es3-syntax.js for the full list.
Covers sections 2.2–2.6 and 3.6.
- Properties (
2.2): the allow-list holds exactly the 90 properties Table 2 marks as supported. - At-rules (
2.3): only@charset,@importand@mediaare allowed, so@font-face,@pageand anything newer are reported. - Pseudo classes (
2.4)::hover,:before,:afterand any::form are reported. - Selectors (
2.5): attribute selectors, adjacent sibling (+) and general sibling (~). - Colors (
2.6):hsl()/hsla(),currentColor, CSS2 system colors, non-HTML4 keywords in color properties, and hexadecimal colors that are not the six-digit form (#fffis out of profile;#ffffffis in). - DOM style properties (
3.6):element.style.<name>is checked against the CSS2Properties subset, sostyle.cursor,style.tableLayout,style.borderSpacing,style.getPropertyCSSValueand modern properties such asstyle.transformare reported.
CSS text is scanned in:
- assignments to
*.style.cssText setAttribute("style", "...")- tagged template literals named
css,styleorstylesheet - string/template variables with
cssorstylein their identifier name <style>blocks in HTML, via thehbbtv-compat/htmlprocessor- whole stylesheets, via the
hbbtv-compat/cssprocessor
The variable scan is a name heuristic. CSS built by concatenation, returned from a function, or
passed straight into insertRule() is not seen.
Findings from a stylesheet are reported against the top-level rule they occur in, not the exact
declaration line: the rule reports on the string that carried the CSS, so the hbbtv-compat/css
processor emits one string per top-level rule to keep that granularity useful.
Inline style attributes are not scanned, and neither are on* handler attributes. The HTML
processor reads <style> blocks and inline <script> bodies.
The processors treat CSS as opaque text and the rule only inspects constructs it recognises, so
template placeholders are neither parsed nor understood — they simply match nothing. A stylesheet
containing color: {{ PRIMARY_COLOR }}; or {% if dark %} lints without special configuration and
without false positives, which is what lets a stylesheet be linted before its template step runs.
This is a consequence of the design, not support for any template language. Brace-delimited syntaxes
(Nunjucks, Jinja, Liquid, Handlebars, Mustache) work because their braces are balanced and so cannot
close a CSS block early. Syntaxes that place a bare word where a value belongs — <%= color %> — can
still be misread as a color keyword by the colour check; that is inherent to the rule's text scanning
and applies equally to CSS embedded in JavaScript.
{
"hbbtv-compat/no-unsupported-css": [
"error",
{
"allowedProperties": ["color", "background-color", "font-size"],
"allowedStyleProperties": ["color", "backgroundColor"],
"allowVendorPrefixed": true,
"scanStyleVariables": true,
"checkFeatures": true,
"checkStyleProperties": true
}
]
}checkFeatures toggles the at-rule, pseudo class, selector and color checks.
checkStyleProperties toggles the element.style.<name> check.
Volume 5 Annex B, "Changes to Section 5.4", adds properties and methods to the Window scripting
object that the Reference Guide's section 3.2 table marks as not supported. Annex B is
normative, so these are allowed:
window.self— "Window self: reference to the current window" SHALL be addedwindow.removeEventListener— "allows DOM 2 event listener de-registration on the Window object"KeyEvent.charCode— Annex B, "In Annex I": "attributecharCodemust be added to theKeyEventinterface"debug()as a bare global — section7.15.5says it "is available on the global (Window) object" and its own example calls it unqualified
Annex B also confirms exclusions the Reference Guide already had: image-orientation is OPTIONAL,
font system values are not required, HTMLAppletElement/HTMLFrameElement/HTMLFrameSetElement
are excluded, and Notation/Entity/EntityReference/ProcessingInstruction are excluded.
getPropertyCSSValue, CSSValue, RGBColor, Rect, Counter and CSSUnknownRule are declared
OPTIONAL rather than forbidden. The plugin still reports them, since a compatibility linter cannot
assume an optional feature is present.
Volume 5 delegates the CSS and DOM style profiles to CEA-2014-A Annexes H and I and does not restate their tables, so these remain open. Resolving them requires CEA-2014-A itself.
table-layoutandborder-spacingare marked supported in section2.2but their CSS2Properties counterpartstableLayoutandborderSpacingare marked unsupported in section3.6. The plugin follows each section for its own surface: the CSS property names are allowed in stylesheet text, the DOM property names are not.:langis marked supported in section2.4but theE:lang(c)selector form is marked unsupported in section2.5. The plugin allows it, since that is the reading that avoids false positives.- Section
3.8lists onlyImageandOptionas constructor aliases, both unsupported. The DOM interface objects that carry constants (Node,DOMException,EventException,MutationEvent,CSSRule) are treated as available globals, restricted to those constants, because that is the only way the section3.3/3.4/3.6constants can be read. - Hexadecimal colors: section
2.1permits only the six-digit form, so#fffis reported. Volume 5 is silent, so this stays as the Reference Guide states it.
npm test