From 784a9a918ff8ce7b902e87cbedb288b101e0fb9b Mon Sep 17 00:00:00 2001 From: Yimiprod Date: Fri, 1 Dec 2023 13:17:11 +0100 Subject: [PATCH 1/2] fix: marked and parse type overload with discrinated union options --- src/Instance.ts | 15 ++++++++++----- src/MarkedOptions.ts | 24 +++++++++++++++++++++--- src/marked.ts | 14 +++++++++++--- test/types/marked.ts | 23 +++++++++++++++-------- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/Instance.ts b/src/Instance.ts index eb460e0642..9b99cd5687 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,11 +270,11 @@ 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 (isAsyncOptions(this.defaults) && isSyncOptions(origOpt)) { 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.'); } @@ -292,7 +297,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..d1b73c779b 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 'async' in options && 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..de2bd4bdd2 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,13 @@ 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 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 defaultMarkedParse: string = marked.parse(md); -// as string can be used if no extensions set `async: true` -const stringMarkedParse: string = marked.parse(md) as string; })(); // Tests for List and ListItem From 66199ad72b42fab87e5f04379401417ae40c51e3 Mon Sep 17 00:00:00 2001 From: Yimiprod Date: Mon, 12 Feb 2024 16:11:01 +0100 Subject: [PATCH 2/2] feat(Instance)!: parser throw when default async is true and called with async at false BREAKING CHANGE: parser now throw an error when `defaultConfig` is modified by an extension to make the returned value a Promise and the we call parser with `async` option at false. It can be skipped with `silent` to true, making it still working for library that use it to modify the config globally but still calling each instance with the option `async` at false. --- src/Instance.ts | 12 ++++-------- src/MarkedOptions.ts | 2 +- test/types/marked.ts | 16 ++++++++++++++-- test/unit/marked.test.js | 14 +++++++++++++- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Instance.ts b/src/Instance.ts index 9b99cd5687..5939531470 100644 --- a/src/Instance.ts +++ b/src/Instance.ts @@ -273,17 +273,13 @@ export class Marked { 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 (isAsyncOptions(this.defaults) && isSyncOptions(origOpt)) { - 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')); diff --git a/src/MarkedOptions.ts b/src/MarkedOptions.ts index d1b73c779b..efd8ee5020 100644 --- a/src/MarkedOptions.ts +++ b/src/MarkedOptions.ts @@ -162,7 +162,7 @@ export interface MarkedAsyncOptions extends _MarkedOptions { export type MarkedOptions = MarkedSyncOptions | MarkedAsyncOptions; export function isAsyncOptions(options: MarkedOptions): options is MarkedAsyncOptions { - return 'async' in options && options.async === true; + return options.async === true; } export function isSyncOptions(options: MarkedOptions): options is MarkedSyncOptions { diff --git a/test/types/marked.ts b/test/types/marked.ts index de2bd4bdd2..414b82d4d7 100644 --- a/test/types/marked.ts +++ b/test/types/marked.ts @@ -261,13 +261,25 @@ marked.use(asyncExtension); const md = '# foobar'; const asyncMarked: string = await marked(md, { async: true }); const promiseMarked: Promise = marked(md, { async: true }); -const notAsyncMarked: string = marked(md, { async: false }); +const notAsyncMarked: string = marked(md, { async: false, silent: true }); const defaultMarked: string = marked(md); const asyncMarkedParse: string = await marked.parse(md, { async: true }); const promiseMarkedParse: Promise = marked.parse(md, { async: true }); -const notAsyncMarkedParse: string = marked.parse(md, { async: false }); +const notAsyncMarkedParse: string = marked.parse(md, { async: false, silent: true }); const defaultMarkedParse: string = marked.parse(md); + +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', () => {