markedjs/marked · error · Error
extension level must be 'block' or 'inline'
Error message
extension level must be 'block' or 'inline'
What it means
Thrown by Marked.use() when an extension that declares a tokenizer ('tokenizer' in ext is true) has a level that is missing, empty, or not exactly 'block' or 'inline'. The level tells marked where to inject the tokenizer: block tokenizers run in Lexer.blockTokens and inline tokenizers run in Lexer.inlineTokens (Lexer.ts:124-131, 353-360). Without a valid level marked cannot route the tokenizer, so registration is aborted. Renderer-only extensions (no tokenizer) do not need a level and never trigger this.
Source
Thrown at src/Instance.ts:131
}
if ('renderer' in ext) { // Renderer extensions
const prevRenderer = extensions.renderers[ext.name];
if (prevRenderer) {
// Replace extension with func to run new extension but fall back if false
extensions.renderers[ext.name] = function(...args) {
let ret = ext.renderer.apply(this, args);
if (ret === false) {
ret = prevRenderer.apply(this, args);
}
return ret;
};
} else {
extensions.renderers[ext.name] = ext.renderer;
}
}
if ('tokenizer' in ext) { // Tokenizer Extensions
if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {
throw new Error("extension level must be 'block' or 'inline'");
}
const extLevel = extensions[ext.level];
if (extLevel) {
extLevel.unshift(ext.tokenizer);
} else {
extensions[ext.level] = [ext.tokenizer];
}
if (ext.start) { // Function to check for start of token
if (ext.level === 'block') {
if (extensions.startBlock) {
extensions.startBlock.push(ext.start);
} else {
extensions.startBlock = [ext.start];
}
} else if (ext.level === 'inline') {
if (extensions.startInline) {
extensions.startInline.push(ext.start);
} else {View on GitHub (pinned to 9552b6bbca)
Solutions
- Add level:'block' or level:'inline' (lowercase) to the extension object.
- If you only want to change rendering of existing tokens, drop the tokenizer and keep renderer plus name (no level needed).
- When building extensions generically, default-fill level from a constant before calling use().
Example fix
// before
marked.use({
extensions: [{
name: 'mention',
tokenizer(src) { const m = /^@(\w+)/.exec(src); if (!m) return; return { type:'mention', raw:m[0], text:m[1], tokens:[] }; }
}]
});
// after
marked.use({
extensions: [{
name: 'mention',
level: 'inline',
tokenizer(src) { const m = /^@(\w+)/.exec(src); if (!m) return; return { type:'mention', raw:m[0], text:m[1], tokens:[] }; }
}]
}); Defensive patterns
Strategy: validation
Validate before calling
for (const ext of myExt.extensions ?? []) {
if ('tokenizer' in ext && ext.level !== 'block' && ext.level !== 'inline') {
throw new Error('extension "' + (ext.name ?? '<unnamed>') + '" needs level: \'block\' | \'inline\'');
}
}
marked.use(myExt); Type guard
function hasValidLevel(ext) {
return !('tokenizer' in ext) || ext.level === 'block' || ext.level === 'inline';
} Prevention
- Treat level as mandatory whenever you write tokenizer: in an extension.
- Use the TokenizerExtension type from marked - it marks level as required.
- Remember renderer-only extensions are exempt from level.
When it happens
Trigger: marked.use({ extensions:[{ name:'x', tokenizer(src){...} }] }) with no level; level:'Block' or 'block-level' (wrong casing/wording, must be lowercase exactly); level:'' or level:undefined; spreading a base extension that had level but overriding it to an invalid value.
Common situations: Copying a renderer-extension example and adding a tokenizer without realising level becomes mandatory; assuming the default level is block (there is no default); writing 'Block' for readability; case-sensitivity mistakes.
Related errors
- extension name required
- tokenizer '${prop}' does not exist
- renderer '${prop}' does not exist
- hook '${prop}' does not exist
- Infinite loop on byte: ${byte}
AI-assisted analysis of markedjs/marked@9552b6bbca (2026-08-13).
Data as JSON: /api/errors/660c14037d5f5f7a.
Report an issue: GitHub.