diff --git a/src/Instance.ts b/src/Instance.ts index eb460e0642..5939531470 100644 --- a/src/Instance.ts +++ b/src/Instance.ts @@ -8,7 +8,12 @@ import { _TextRenderer } from './TextRenderer.ts'; import { escape } from './helpers.ts'; -import type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts'; +import { + isAsyncOptions, + isSyncOptions, + type MarkedExtension, + type MarkedOptions +} from './MarkedOptions.ts'; import type { Token, Tokens, TokensList } from './Tokens.ts'; export type MaybePromise = void | Promise; @@ -265,20 +270,16 @@ export class Marked { #parseMarkdown(lexer: (src: string, options?: MarkedOptions) => TokensList | Token[], parser: (tokens: Token[], options?: MarkedOptions) => string) { return (src: string, options?: MarkedOptions | undefined | null): string | Promise => { - const origOpt = { ...options }; - const opt = { ...this.defaults, ...origOpt }; + const origOpt: MarkedOptions = { ...options }; + const opt: MarkedOptions = { ...this.defaults, ...origOpt }; - // Show warning if an extension set async to true but the parse was called with async: false - if (this.defaults.async === true && origOpt.async === false) { - if (!opt.silent) { - console.warn('marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored.'); - } + const throwError = this.#onError(!!opt.silent, !!opt.async); - opt.async = true; + if (isAsyncOptions(this.defaults) && isSyncOptions(origOpt)) { + // Throw an error if an extension set async to true but the parse was called with async: false + return throwError(new Error('marked(): The async option was set to true by an extension. Remove the async: false option to continue.')); } - const throwError = this.#onError(!!opt.silent, !!opt.async); - // throw error in case of non string input if (typeof src === 'undefined' || src === null) { return throwError(new Error('marked(): input parameter is undefined or null')); @@ -292,7 +293,7 @@ export class Marked { opt.hooks.options = opt; } - if (opt.async) { + if (isAsyncOptions(opt)) { return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) .then(src => lexer(src, opt)) .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens) diff --git a/src/MarkedOptions.ts b/src/MarkedOptions.ts index 78754f23fe..efd8ee5020 100644 --- a/src/MarkedOptions.ts +++ b/src/MarkedOptions.ts @@ -64,8 +64,8 @@ export interface MarkedExtension { * Add tokenizers and renderers to marked */ extensions?: - | TokenizerAndRendererExtension[] - | undefined | null; + | TokenizerAndRendererExtension[] + | undefined | null; /** * Enable GitHub flavored markdown. @@ -111,7 +111,7 @@ export interface MarkedExtension { walkTokens?: ((token: Token) => void | Promise) | undefined | null; } -export interface MarkedOptions extends Omit { +interface _MarkedOptions extends Omit { /** * Hooks are methods that hook into some part of marked. */ @@ -150,3 +150,21 @@ export interface MarkedOptions extends Omit void | Promise | (void | Promise)[]); } + +export interface MarkedSyncOptions extends _MarkedOptions { + async?: false; +} + +export interface MarkedAsyncOptions extends _MarkedOptions { + async: true; +} + +export type MarkedOptions = MarkedSyncOptions | MarkedAsyncOptions; + +export function isAsyncOptions(options: MarkedOptions): options is MarkedAsyncOptions { + return options.async === true; +} + +export function isSyncOptions(options: MarkedOptions): options is MarkedSyncOptions { + return !isAsyncOptions(options); +} diff --git a/src/marked.ts b/src/marked.ts index 51fe97ee78..371925b3e3 100644 --- a/src/marked.ts +++ b/src/marked.ts @@ -10,7 +10,7 @@ import { changeDefaults, _defaults } from './defaults.ts'; -import type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts'; +import type { MarkedExtension, MarkedOptions, MarkedAsyncOptions, MarkedSyncOptions } from './MarkedOptions.ts'; import type { Token, TokensList } from './Tokens.ts'; import type { MaybePromise } from './Instance.ts'; @@ -23,8 +23,15 @@ const markedInstance = new Marked(); * @param options Hash of options, having async: true * @return Promise of string of compiled HTML */ -export function marked(src: string, options: MarkedOptions & { async: true }): Promise; - +export function marked(src: string, options: MarkedAsyncOptions): Promise; +/** + * Compiles markdown to HTML synchronously. + * + * @param src String of markdown source to be compiled + * @param options Hash of options, having async: false or undefined + * @return String of compiled HTML + */ +export function marked(src: string, options?: MarkedSyncOptions): string; /** * Compiles markdown to HTML. * @@ -115,5 +122,6 @@ export { _TextRenderer as TextRenderer } from './TextRenderer.ts'; export { _Hooks as Hooks } from './Hooks.ts'; export { Marked } from './Instance.ts'; export type * from './MarkedOptions.ts'; +export { isSyncOptions, isAsyncOptions } from './MarkedOptions.ts'; export type * from './rules.ts'; export type * from './Tokens.ts'; diff --git a/test/types/marked.ts b/test/types/marked.ts index 8bf2997b2e..414b82d4d7 100644 --- a/test/types/marked.ts +++ b/test/types/marked.ts @@ -3,6 +3,7 @@ import { marked } from 'marked'; // other exports +import { isAsyncOptions, isSyncOptions } from 'marked'; import { Lexer, Parser, Tokenizer, Renderer, TextRenderer } from 'marked'; import type { Tokens, MarkedExtension, TokenizerAndRendererExtension, Token ,TokenizerExtension, MarkedOptions, TokensList, RendererExtension } from 'marked'; @@ -89,6 +90,20 @@ renderer.checkbox = checked => { return checked ? 'CHECKED' : 'UNCHECKED'; }; +options = {...options, async: false}; + +if (isSyncOptions(options)) { + console.log(await marked.parseInline('12) I am using __markdown__.', options)); +} + +options = {...options, async: true}; + +if (isAsyncOptions(options)) { + (async () => { + console.log(await marked.parseInline('12) I am using __markdown__.', options)); + })() +} + class ExtendedRenderer extends marked.Renderer { code = (code: string, language: string | undefined, isEscaped: boolean): string => super.code(code, language, isEscaped); blockquote = (quote: string): string => super.blockquote(quote); @@ -246,21 +261,25 @@ marked.use(asyncExtension); const md = '# foobar'; const asyncMarked: string = await marked(md, { async: true }); const promiseMarked: Promise = marked(md, { async: true }); -// @ts-expect-error marked can still be async if an extension sets `async: true` -const notAsyncMarked: string = marked(md, { async: false }); -// @ts-expect-error marked can still be async if an extension sets `async: true` +const notAsyncMarked: string = marked(md, { async: false, silent: true }); const defaultMarked: string = marked(md); -// as string can be used if no extensions set `async: true` -const stringMarked: string = marked(md) as string; const asyncMarkedParse: string = await marked.parse(md, { async: true }); const promiseMarkedParse: Promise = marked.parse(md, { async: true }); -// @ts-expect-error marked can still be async if an extension sets `async: true` -const notAsyncMarkedParse: string = marked.parse(md, { async: false }); -// @ts-expect-error marked can still be async if an extension sets `async: true` +const notAsyncMarkedParse: string = marked.parse(md, { async: false, silent: true }); const defaultMarkedParse: string = marked.parse(md); -// as string can be used if no extensions set `async: true` -const stringMarkedParse: string = marked.parse(md) as string; + +try { + const notAsyncMarkedThrow: string = marked(md, { async: false, silent: false }); +} catch { + console.log('expected throw'); +} + +try { + const notAsyncMarkedParseThrow: string = marked.parse(md, { async: false, silent: false }); +} catch { + console.log('expected throw'); +} })(); // Tests for List and ListItem diff --git a/test/unit/marked.test.js b/test/unit/marked.test.js index c9c0925a12..fec38b10b1 100644 --- a/test/unit/marked.test.js +++ b/test/unit/marked.test.js @@ -632,7 +632,19 @@ used extension2 walked

it('should return Promise if async is set by extension', () => { marked.use({ async: true }); - assert.ok(marked.parse('test', { async: false }) instanceof Promise); + assert.ok(marked.parse('test') instanceof Promise); + }); + + it('should throw an if async is set by extension and a different async parameter is set', () => { + marked.use({ async: true }); + + assert.throws(() => marked.parse('test', { async: false }), /The async option was set to true by an extension/); + }); + + it('should return a string error message if async is set by extension and a different async parameter is set and the silent parameter is set', () => { + marked.use({ async: true }); + + assert.match(marked.parse('test', { async: false, silent: true }), /The async option was set to true by an extension/); }); it('should allow deleting/editing tokens', () => {