Skip to main content

Using to besides HTML

You can also apply it to syntaxes besides HTML such as template engines or frameworks if using plugins together.

Installing plugins

Install the parser plugin through npm or Yarn:

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 a plugin to apply to the parser property on the configuration file. And If it has spec add to the specs property. Set a regular expression that can identify the target file name to the parser property key.

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 explained configuring parser and specs if you want details.

Why need the spec plugins?

For example, the key attribute doesn't exist in native HTML elements. But you often need to specify it when you use React or Vue. So you should specify @markuplint/react-spec or @markuplint/vue-spec.

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>

Besides, spec plugins include specific attributes and directives each owned.

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">