Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/USING_ADVANCED.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ console.log(marked.parse(markdownString));
|smartypants (**removed**)|`boolean` |`false` |v0.2.9 |Removed in v8.0.0 use [`marked-smartypants`](https://www.npmjs.com/package/marked-smartypants) to use "smart" typographic punctuation for things like quotes and dashes.|
|xhtml (**removed**)|`boolean` |`false` |v0.3.2 |Removed in v8.0.0 use [`marked-xhtml`](https://www.npmjs.com/package/marked-xhtml) to emit self-closing HTML tags for void elements (<br/>, <img/>, etc.) with a "/" as required by XHTML.|

<h2 id="extensions">Known Extensions</h2>
<h2 id="extensions">Known MarkedExtensions (Plugins)</h2>

Marked can be extended using [custom extensions](/using_pro#extensions). This is a list of extensions that can be used with `marked.use(extension)`.
Marked can be extended using [MarkedExtensions (plugins)](/using_pro#extensions). These are complete plugin packages that can be used with `marked.use(extension)`. They are different from [custom tokenizer and renderer extensions](/using_pro#extensions) which are individual tokenizer/renderer pairs that go inside the `tokenizerAndRendererExtensions` array.

<!-- Keep this list ordered alphabetically by name -->

Expand Down
12 changes: 6 additions & 6 deletions docs/USING_PRO.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ marked.use(extension2);
marked.use(extension3);
```

All options will overwrite those previously set, except for the following options which will be merged with the existing framework and can be used to change or extend the functionality of Marked: `renderer`, `tokenizer`, `hooks`, `walkTokens`, and `extensions`.
All options will overwrite those previously set, except for the following options which will be merged with the existing framework and can be used to change or extend the functionality of Marked: `renderer`, `tokenizer`, `hooks`, `walkTokens`, and `tokenizerAndRendererExtensions` (or `extensions` for backward compatibility).

* The `renderer`, `tokenizer`, and `hooks` options are objects with functions that will be merged into the built-in `renderer` and `tokenizer` respectively.

* The `walkTokens` option is a function that will be called to post-process every token before rendering.

* The `extensions` option is an array of objects that can contain additional custom `renderer` and `tokenizer` steps that will execute before any of the default parsing logic occurs.
* The `tokenizerAndRendererExtensions` option (or `extensions` for backward compatibility) is an array of objects that can contain additional custom `renderer` and `tokenizer` steps that will execute before any of the default parsing logic occurs.

Importantly, ensure that the extensions are only added to `marked` once (ie in the global scope of a regular JavaScript or TypeScript module). If they are added in a function that is called repeatedly, or in the JS for an HTML component in a library such as Svelte, your extensions will be added repeatedly, eventually causing a recursion error. If you cannot prevent the code from being run repeatedly, you should create a [Marked instance](/using_advanced#instance) so that your extensions are stored independently from the global instance Marked provides.

Expand All @@ -52,7 +52,7 @@ Before building your custom extensions, it is important to understand the compon
4) The `parser` traverses the token tree and feeds each token into the appropriate `renderer`, and concatenates their outputs into the final HTML result.
5) Each `renderer` receives a token and manipulates its contents to generate a segment of HTML.

Marked provides methods for directly overriding the `renderer` and `tokenizer` for any existing token type, as well as inserting additional custom `renderer` and `tokenizer` functions to handle entirely custom syntax. For example, using `marked.use({renderer})` would modify a renderer, whereas `marked.use({extensions: [{renderer}]})` would add a new renderer. See the [custom extensions example](#custom-extensions-example) for insight on how to execute this.
Marked provides methods for directly overriding the `renderer` and `tokenizer` for any existing token type, as well as inserting additional custom `renderer` and `tokenizer` functions to handle entirely custom syntax. For example, using `marked.use({renderer})` would modify a renderer, whereas `marked.use({tokenizerAndRendererExtensions: [{renderer}]})` would add a new renderer. See the [custom extensions example](#custom-extensions-example) for insight on how to execute this.

***

Expand Down Expand Up @@ -104,7 +104,7 @@ console.log(marked.parse('# heading+'));

```js
marked.use({
extensions: [{
tokenizerAndRendererExtensions: [{
name: 'heading',
renderer(token) {
return /* ... */
Expand Down Expand Up @@ -392,9 +392,9 @@ console.log(marked.parse(`_The formula is $a_ b=c_ d$._`));

***

<h2 id="extensions">Custom Extensions : <code>extensions</code></h2>
<h2 id="extensions">Custom Tokenizer and Renderer Extensions : <code>tokenizerAndRendererExtensions</code></h2>

You may supply an `extensions` array to the `options` object. This array can contain any number of `extension` objects, using the following properties:
You may supply a `tokenizerAndRendererExtensions` array to the `options` object. This array can contain any number of `extension` objects, using the following properties:

<dl>
<dt><code><strong>name</strong></code></dt>
Expand Down
9 changes: 6 additions & 3 deletions src/Instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class Marked<ParserOutput = string, RendererOutput = string> {
}

use(...args: MarkedExtension<ParserOutput, RendererOutput>[]) {
const extensions: MarkedOptions<ParserOutput, RendererOutput>['extensions'] = this.defaults.extensions || { renderers: {}, childTokens: {} };
const extensions: MarkedOptions<ParserOutput, RendererOutput>['tokenizerAndRendererExtensions'] = this.defaults.tokenizerAndRendererExtensions || this.defaults.extensions || { renderers: {}, childTokens: {} };

args.forEach((pack) => {
// copy options to new object
Expand All @@ -84,8 +84,9 @@ export class Marked<ParserOutput = string, RendererOutput = string> {
opts.async = this.defaults.async || opts.async || false;

// ==-- Parse "addon" extensions --== //
if (pack.extensions) {
pack.extensions.forEach((ext) => {
const extensionsArray = pack.tokenizerAndRendererExtensions || pack.extensions;
if (extensionsArray) {
extensionsArray.forEach((ext) => {
if (!ext.name) {
throw new Error('extension name required');
}
Expand Down Expand Up @@ -134,6 +135,8 @@ export class Marked<ParserOutput = string, RendererOutput = string> {
extensions.childTokens[ext.name] = ext.childTokens;
}
});
opts.tokenizerAndRendererExtensions = extensions;
// Also set the deprecated property for backward compatibility
opts.extensions = extensions;
}

Expand Down
29 changes: 27 additions & 2 deletions src/MarkedOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ export interface MarkedExtension<ParserOutput = string, RendererOutput = string>
/**
* Add tokenizers and renderers to marked
*/
tokenizerAndRendererExtensions?:
| TokenizerAndRendererExtension<ParserOutput, RendererOutput>[]
| null;

/**
* Add tokenizers and renderers to marked
* @deprecated Use tokenizerAndRendererExtensions instead
*/
extensions?:
| TokenizerAndRendererExtension<ParserOutput, RendererOutput>[]
| null;
Expand Down Expand Up @@ -114,7 +122,7 @@ export interface MarkedExtension<ParserOutput = string, RendererOutput = string>
walkTokens?: ((token: Token) => void | Promise<void>) | null;
}

export interface MarkedOptions<ParserOutput = string, RendererOutput = string> extends Omit<MarkedExtension<ParserOutput, RendererOutput>, 'hooks' | 'renderer' | 'tokenizer' | 'extensions' | 'walkTokens'> {
export interface MarkedOptions<ParserOutput = string, RendererOutput = string> extends Omit<MarkedExtension<ParserOutput, RendererOutput>, 'hooks' | 'renderer' | 'tokenizer' | 'tokenizerAndRendererExtensions' | 'extensions' | 'walkTokens'> {
/**
* Hooks are methods that hook into some part of marked.
*/
Expand All @@ -133,7 +141,24 @@ export interface MarkedOptions<ParserOutput = string, RendererOutput = string> e
tokenizer?: _Tokenizer<ParserOutput, RendererOutput> | null;

/**
* Custom extensions
* Custom tokenizer and renderer extensions
*/
tokenizerAndRendererExtensions?: null | {
renderers: {
[name: string]: RendererExtensionFunction<ParserOutput, RendererOutput>;
};
childTokens: {
[name: string]: string[];
};
inline?: TokenizerExtensionFunction[];
block?: TokenizerExtensionFunction[];
startInline?: TokenizerStartFunction[];
startBlock?: TokenizerStartFunction[];
};

/**
* Custom tokenizer and renderer extensions
* @deprecated Use tokenizerAndRendererExtensions instead
*/
extensions?: null | {
renderers: {
Expand Down
1 change: 1 addition & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function _getDefaults<ParserOutput = string, RendererOutput = string>():
async: false,
breaks: false,
extensions: null,
tokenizerAndRendererExtensions: null,
gfm: true,
hooks: null,
pedantic: false,
Expand Down
5 changes: 5 additions & 0 deletions test/types/marked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ marked.use({
extensions: [tokenizerExtension, rendererExtension, tokenizerAndRendererExtension]
});

// Test new property name
marked.use({
tokenizerAndRendererExtensions: [tokenizerExtension, rendererExtension, tokenizerAndRendererExtension]
});

const asyncExtension: MarkedExtension = {
async: true,
async walkTokens(token) {
Expand Down
44 changes: 44 additions & 0 deletions test_new_property.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { marked } from './src/marked.ts';

// Test that both property names work
const testExtension = {
name: 'test',
level: 'inline',
start(src) { return src.indexOf('TEST'); },
tokenizer(src) {
const match = src.match(/^TEST/);
if (match) {
return {
type: 'test',
raw: match[0],
text: 'TESTED'
};
}
},
renderer(token) {
return `<span class="test">${token.text}</span>`;
}
};

// Test old property name (should still work)
console.log('Testing old property name (extensions):');
try {
marked.use({ extensions: [testExtension] });
console.log('✓ Old property name works');
console.log('Result:', marked.parse('This is TEST content'));
} catch (e) {
console.log('✗ Old property name failed:', e.message);
}

// Reset marked
marked.setOptions(marked.getDefaults());

// Test new property name
console.log('\nTesting new property name (tokenizerAndRendererExtensions):');
try {
marked.use({ tokenizerAndRendererExtensions: [testExtension] });
console.log('✓ New property name works');
console.log('Result:', marked.parse('This is TEST content'));
} catch (e) {
console.log('✗ New property name failed:', e.message);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While the current test correctly validates both property names by resetting the global marked instance, this approach of mutating and resetting global state can make tests brittle and harder to maintain. A better practice for test isolation is to use separate Marked instances for each test case. This avoids side effects between tests and makes them more robust. The suggested change refactors the test to use this approach, and also updates the import statement accordingly.

import { Marked } from './src/marked.ts';

// Test that both property names work
const testExtension = {
  name: 'test',
  level: 'inline',
  start(src) { return src.indexOf('TEST'); },
  tokenizer(src) {
    const match = src.match(/^TEST/);
    if (match) {
      return {
        type: 'test',
        raw: match[0],
        text: 'TESTED'
      };
    }
  },
  renderer(token) {
    return `<span class="test">${token.text}</span>`;
  }
};

// Test old property name (should still work)
console.log('Testing old property name (extensions):');
try {
  const markedOld = new Marked({ extensions: [testExtension] });
  console.log('✓ Old property name works');
  console.log('Result:', markedOld.parse('This is TEST content'));
} catch (e) {
  console.log('✗ Old property name failed:', e.message);
}

// Test new property name
console.log('\nTesting new property name (tokenizerAndRendererExtensions):');
try {
  const markedNew = new Marked({ tokenizerAndRendererExtensions: [testExtension] });
  console.log('✓ New property name works');
  console.log('Result:', markedNew.parse('This is TEST content'));
} catch (e) {
  console.log('✗ New property name failed:', e.message);
}