Skip to main content

Beyond HTML

Markuplint can lint syntaxes beyond HTML — including JSX, Vue, Svelte, Pug, PHP, and more — by using parser and spec plugins.

Installing plugins

Install the parser plugin through your package manager:

npm install -D @markuplint/pug-parser

If your code uses tagged template literals containing HTML (e.g., lit-html), install the tagged template literal parser:

npm install -D @markuplint/tagged-template-literal-parser

If a syntax has its own specification you should install the spec plugin with the parser plugin:

npm install -D @markuplint/jsx-parser @markuplint/react-spec
npm install -D @markuplint/vue-parser @markuplint/vue-spec

Supported syntaxes

Template or syntaxParserSpec
JSX@markuplint/jsx-parser@markuplint/react-spec
Vue@markuplint/vue-parser@markuplint/vue-spec
Svelte@markuplint/svelte-parser@markuplint/svelte-spec
SvelteKit@markuplint/svelte-parser/kit-
Astro@markuplint/astro-parser-
Alpine.js@markuplint/alpine-parser@markuplint/alpine-spec
HTMX-@markuplint/htmx-spec
Tagged template literals (lit-html etc.)@markuplint/tagged-template-literal-parser-
Markdown@markuplint/markdown-parser-
MDX@markuplint/mdx-parser@markuplint/react-spec
Pug@markuplint/pug-parser-
PHP@markuplint/php-parser-
Smarty@markuplint/smarty-parser-
eRuby@markuplint/erb-parser-
EJS@markuplint/ejs-parser-
Mustache or Handlebars@markuplint/mustache-parser-
Nunjucks@markuplint/nunjucks-parser-
Liquid@markuplint/liquid-parser-
note

There is @markuplint/html-parser package but the core package includes it. You don't need to install and to specify it to the configuration.

Unsupported syntaxes

It's not able to support syntaxes if one's attribute is complex.

✅ Available code

<div attr="{{ value }}"></div>
<div attr='{{ value }}'></div>
<div attr="{{ value }}-{{ value2 }}-{{ value3 }}"></div>

❌ Unavailable code

If it doesn't nest by quotations.

<div attr={{ value }}></div>

PULL REQUEST WANTED: This problem is recognized by developers and created as an issue #240.

Applying plugins

Specify the plugin in the parser property of your configuration file. If the syntax has a spec plugin, add it to the specs property as well. Use a regular expression as the key to match target file names.

Use React
{
"parser": {
"\\.jsx$": "@markuplint/jsx-parser"
},
"specs": {
"\\.jsx$": "@markuplint/react-spec"
}
}
Use Vue
{
"parser": {
"\\.vue$": "@markuplint/vue-parser"
},
"specs": {
"\\.vue$": "@markuplint/vue-spec"
}
}
Use lit-html
{
"parser": {
"\\.ts$": "@markuplint/tagged-template-literal-parser"
}
}
Use Markdown
{
"parser": {
"\\.md$": "@markuplint/markdown-parser"
}
}
Use MDX
{
"parser": {
"\\.mdx$": "@markuplint/mdx-parser"
},
"specs": {
"\\.mdx$": "@markuplint/react-spec"
}
}

See the parser and specs property references for details.

Why need the spec plugins?

For example, the key attribute doesn't exist in native HTML elements, but it's commonly used in React and Vue. Spec plugins like @markuplint/react-spec and @markuplint/vue-spec tell Markuplint about these framework-specific attributes.

const Component = ({ list }) => {
return (
<ul>
{list.map(item => (
<li key={item.key}>{item.text}</li>
))}
</ul>
);
};
<template>
<ul>
<li v-for="item in list" :key="item.key">{{ item.text }}</li>
</ul>
</template>

In addition, spec plugins include definitions for framework-specific attributes and directives.

Pretenders

In React, Vue, and more, custom components cannot be evaluated as HTML elements. This means Markuplint's content model rules — such as permitted-contents — have no way of knowing what a component actually renders. Without this information, a <Button> component that renders a <button> element is treated as an unknown element, and invalid nesting like <a><Button /></a> (interactive content inside interactive content) goes undetected.

<List>{/* No evaluate as native HTML Element */}
<Item />{/* No evaluate as native HTML Element */}
<Item />{/* No evaluate as native HTML Element */}
<Item />{/* No evaluate as native HTML Element */}
</List>

The Pretenders feature resolves that by telling Markuplint what each component renders as.

Manual configuration

You can manually specify a selector for each component and the HTML element it renders:

{
"pretenders": [
{
"selector": "List",
"as": "ul"
},
{
"selector": "Item",
"as": "li"
}
]
}
<List>{/* Evaluate as <ul> */}
<Item />{/* Evaluate as <li> */}
<Item />{/* Evaluate as <li> */}
<Item />{/* Evaluate as <li> */}
</List>

This works well for small projects, but manually maintaining the list becomes tedious as your component library grows. That's where dynamic scanning comes in.

See the details of pretenders property on the configuration if you want.

Dynamic scanning

Experimental

This feature is experimental and may change in future releases.

Instead of manually listing every component, you can let Markuplint scan your component source files and discover pretender mappings automatically.

{
"pretenders": {
"scan": [
{
"files": "./src/components/**/*.tsx"
}
]
}
}

This single configuration replaces what might otherwise be dozens of manual pretender entries. When Markuplint runs, it analyzes your component files and determines:

  • Which HTML element each component renders as its root element
  • Whether the component accepts children (slots detection)
  • Static attributes on the root element

Supported file types

File extensions determine the scanner automatically:

ExtensionsScannerFrameworks
.js, .jsx, .ts, .tsxJSX scannerReact, Preact, Solid, etc.
.vueTemplate scannerVue
.svelteTemplate scannerSvelte
.astroTemplate scannerAstro

You can scan multiple file types at once:

{
"pretenders": {
"scan": [
{
"files": "./src/components/**/*.tsx"
},
{
"files": "./src/components/**/*.vue",
"ignoreComponentNames": ["BaseLayout"]
}
]
}
}

What the scanner detects

Consider the following React component:

const ProfileCard = ({ children }) => {
return <article className="profile">{children}</article>;
};

The scanner automatically discovers that ProfileCard renders as <article> and accepts children. This is equivalent to writing:

{
"selector": "ProfileCard",
"as": {
"element": "article",
"slots": true
}
}

Now Markuplint can correctly validate that <ProfileCard> contains only flow content (as <article> does), and that nesting <ProfileCard> inside a <p> would be invalid.

Combining scan with manual definitions

You can use scan alongside manual data definitions. This is useful when the scanner cannot determine the correct mapping for a particular component, or when you want to override the scanned result:

{
"pretenders": {
"scan": [
{
"files": "./src/components/**/*.tsx"
}
],
"data": [
{
"selector": "SpecialComponent",
"as": {
"element": "nav",
"aria": { "name": { "fromAttr": "label" } }
}
}
]
}
}

See pretenders.scan for the full configuration reference.

The as attribute

If a component has the as attribute, it is evaluated as the element specified by this attribute.

<x-ul as="ul"><!-- Evaluate as <ul> -->
<x-li as="li"></x-li><!-- Evaluate as <li> -->
<x-li as="li"></x-li><!-- Evaluate as <li> -->
<x-li as="li"></x-li><!-- Evaluate as <li> -->
</x-ul>

This evaluation also applies to its attributes that are inherited from the component.

<!-- Evaluate as <img src="image.png" alt="image"> -->
<x-img src="image.png" alt="image">

Next steps