diff --git a/lib/extend/filter.ts b/lib/extend/filter.ts index 98b34de1f3..09a6496eb8 100644 --- a/lib/extend/filter.ts +++ b/lib/extend/filter.ts @@ -1,4 +1,5 @@ import Promise from 'bluebird'; +import { extend_filter_before_post_render_data } from '../plugins/filter/before_post_render/dataType'; const typeAlias = { pre: 'before_post_render', @@ -18,7 +19,7 @@ interface StoreFunction { } interface Store { - [key: string]: StoreFunction[] + [key: string]: StoreFunction[]; } class Filter { @@ -35,6 +36,20 @@ class Filter { return this.store[type] || []; } + register(type: string, fn: StoreFunction): void; + register( + type: 'server_middleware', + fn: (app: import('connect').Server) => void + ): void; + register( + type: 'before_post_render', + fn: (data: extend_filter_before_post_render_data) => void + ): void; + register( + type: 'before_post_render', + fn: (data: extend_filter_before_post_render_data) => Promise + ): void; + register(type: string, fn: StoreFunction, priority: number): void; register(fn: StoreFunction): void register(fn: StoreFunction, priority: number): void register(type: string, fn: StoreFunction): void @@ -85,10 +100,12 @@ class Filter { args.unshift(data); - return Promise.each(filters, filter => Reflect.apply(Promise.method(filter), ctx, args).then(result => { - args[0] = result == null ? args[0] : result; - return args[0]; - })).then(() => args[0]); + return Promise.each(filters, filter => + Reflect.apply(Promise.method(filter), ctx, args).then(result => { + args[0] = result == null ? args[0] : result; + return args[0]; + }) + ).then(() => args[0]); } execSync(type: string, data: any, options: FilterOptions = {}) { diff --git a/lib/extend/generator.ts b/lib/extend/generator.ts index 125d2d3de9..6b3fa2ea05 100644 --- a/lib/extend/generator.ts +++ b/lib/extend/generator.ts @@ -10,13 +10,13 @@ type ReturnType = BaseObj | BaseObj[]; type GeneratorReturnType = ReturnType | Promise; interface GeneratorFunction { - (locals: object, callback?: NodeJSLikeCallback): GeneratorReturnType; + (locals: Record, callback?: NodeJSLikeCallback): GeneratorReturnType; } type StoreFunctionReturn = Promise; interface StoreFunction { - (locals: object): StoreFunctionReturn; + (locals: Record): StoreFunctionReturn; } interface Store { diff --git a/lib/extend/helper.ts b/lib/extend/helper.ts index f167282371..f2dd8a8a93 100644 --- a/lib/extend/helper.ts +++ b/lib/extend/helper.ts @@ -1,12 +1,11 @@ interface StoreFunction { - (...args: any[]): string; + (this: import('../hexo'), ...args: any[]): any; } interface Store { - [key: string]: StoreFunction + [key: string]: StoreFunction; } - class Helper { public store: Store; diff --git a/lib/extend/processor.ts b/lib/extend/processor.ts index ce73da1031..396a44aefe 100644 --- a/lib/extend/processor.ts +++ b/lib/extend/processor.ts @@ -28,7 +28,7 @@ class Processor { register(pattern: patternType | StoreFunction, fn?: StoreFunction): void { if (!fn) { if (typeof pattern === 'function') { - fn = pattern; + fn = pattern as StoreFunction; pattern = /(.*)/; } else { throw new TypeError('fn must be a function'); diff --git a/lib/extend/renderer.ts b/lib/extend/renderer.ts index 530b071e18..4c3923f240 100644 --- a/lib/extend/renderer.ts +++ b/lib/extend/renderer.ts @@ -82,10 +82,50 @@ class Renderer { return renderer ? renderer.output : ''; } + /** + * register renderer engine + * - [hexo-renderer-nunjucks example](https://github.com/hexojs/hexo-renderer-nunjucks/blob/c71a1979535c47c3949ff6bf3a85691641841e12/lib/renderer.js#L55-L56) + * - [typescript example](https://github.com/dimaslanjaka/hexo-renderers/blob/feed801e90920bea8a5e7000b275b912e4ef6c43/src/renderer-sass.ts#L37-L38) + * @param name input extension name. ex: ejs + * @param output output extension name. ex: html + * @param fn renderer function + */ register(name: string, output: string, fn: StoreFunction): void; + + /** + * register renderer engine asynchronous + * @param name input extension name. ex: ejs + * @param output output extension name. ex: html + * @param fn renderer asynchronous function + * @param sync is synchronous? + */ register(name: string, output: string, fn: StoreFunction, sync: false): void; + + /** + * register renderer engine + * @param name input extension name. ex: ejs + * @param output output extension name. ex: html + * @param fn renderer function + * @param sync is synchronous? + */ register(name: string, output: string, fn: StoreSyncFunction, sync: true): void; + + /** + * register renderer engine + * @param name input extension name. ex: ejs + * @param output output extension name. ex: html + * @param fn renderer function + * @param sync is synchronous? + */ register(name: string, output: string, fn: StoreFunction | StoreSyncFunction, sync: boolean): void; + + /** + * register renderer engine + * @param name input extension name. ex: ejs + * @param output output extension name. ex: html + * @param fn renderer function + * @param sync is synchronous? + */ register(name: string, output: string, fn: StoreFunction | StoreSyncFunction, sync?: boolean) { if (!name) throw new TypeError('name is required'); if (!output) throw new TypeError('output is required'); diff --git a/lib/extend/syntax_highlight.ts b/lib/extend/syntax_highlight.ts index c5cf28a95f..34bf5f799e 100644 --- a/lib/extend/syntax_highlight.ts +++ b/lib/extend/syntax_highlight.ts @@ -1,12 +1,11 @@ import type Hexo from '../hexo'; export interface HighlightOptions { - lang: string | undefined, - caption: string | undefined, - lines_length?: number | undefined, - // plugins/filter/before_post_render/backtick_code_block - firstLineNumber?: string | number + firstLineNumber?: string | number; + lang: string | undefined; + caption: string | undefined; + lines_length?: number | undefined; // plugins/tag/code.ts language_attr?: boolean | undefined; @@ -15,7 +14,6 @@ export interface HighlightOptions { line_threshold?: number | undefined; mark?: number[]; wrap?: boolean | undefined; - } interface HighlightExecArgs { @@ -29,7 +27,7 @@ interface StoreFunction { } interface Store { - [key: string]: StoreFunction + [key: string]: StoreFunction; } class SyntaxHighlight { @@ -52,7 +50,9 @@ class SyntaxHighlight { exec(name: string, options: HighlightExecArgs): string { const fn = this.store[name]; - if (!fn) throw new TypeError(`syntax highlighter ${name} is not registered`); + if (!fn) { + throw new TypeError(`syntax highlighter ${name} is not registered`); + } const ctx = options.context; const args = options.args || []; diff --git a/lib/extend/tag.ts b/lib/extend/tag.ts index b9648d3297..bad7bb966e 100644 --- a/lib/extend/tag.ts +++ b/lib/extend/tag.ts @@ -8,13 +8,41 @@ const rSwigRawFullBlock = /{% *raw *%}/; const rCodeTag = /]*>[\s\S]+?<\/code>/g; const escapeSwigTag = (str: string) => str.replace(/{/g, '{').replace(/}/g, '}'); -interface TagFunction { - (args: any[], content: string, callback?: NodeJSLikeCallback): string | PromiseLike; -} -interface AsyncTagFunction { - (args: any[], content: string): Promise; +interface ExtendedTagProperty { + + /** + * current absolute file source + */ + full_source: string; } +/** + * synchronous callback - shortcode tag + * @example + * // to get args type + * type args = Parameters[1]>[0]; + * // to get content type + * type content = Parameters[1]>[1]; + */ +type TagFunction = + | ((this: ExtendedTagProperty, arg: string) => string) + | ((this: ExtendedTagProperty, ...args: any[]) => string) + | ((this: ExtendedTagProperty, args: any[], content: string) => string) + | ((args: any[], content: string, callback?: NodeJSLikeCallback) => string | PromiseLike); + +/** + * asynchronous callback - shortcode tag + * @example + * // to get args type + * type args = Parameters[1]>[0]; + * // to get content type + * type content = Parameters[1]>[1]; + */ +type AsyncTagFunction = + | ((this: ExtendedTagProperty, content: string) => PromiseLike | Promise) + | ((this: ExtendedTagProperty, ...args: any[]) => PromiseLike | Promise) + | ((this: ExtendedTagProperty, args: any[], content: string) => PromiseLike | Promise); + class NunjucksTag { public tags: string[]; public fn: TagFunction | AsyncTagFunction; @@ -59,7 +87,7 @@ class NunjucksTag { return node; } - run(context, args, body, callback) { + run(context, args, _body, _callback) { return this._run(context, args, ''); } @@ -80,14 +108,14 @@ class NunjucksBlock extends NunjucksTag { return new nodes.CallExtension(this, 'run', node, [body]); } - _parseBody(parser, nodes, lexer) { + _parseBody(parser, _nodes?, _lexer?) { const body = parser.parseUntilBlocks(`end${this.tags[0]}`); parser.advanceAfterBlockEnd(); return body; } - run(context, args, body, callback) { + run(context, args, body, _callback?) { return this._run(context, args, trimBody(body)); } } @@ -147,18 +175,16 @@ const getContext = (lines: string[], errLine: number, location: string, type: st message.push( // get LINES_OF_CONTEXT lines surrounding `errLine` - ...getContextLineNums(1, lines.length, errLine, LINES_OF_CONTEXT) - .map(lnNum => { - const line = ' ' + lnNum + ' | ' + lines[lnNum - 1]; - if (lnNum === errLine) { - return cyan(bold(line)); - } + ...getContextLineNums(1, lines.length, errLine, LINES_OF_CONTEXT).map(lnNum => { + const line = ' ' + lnNum + ' | ' + lines[lnNum - 1]; + if (lnNum === errLine) { + return cyan(bold(line)); + } - return cyan(line); - }) + return cyan(line); + }) ); - message.push(cyan( - ' ===== Context Dump Ends =====')); + message.push(cyan(' ===== Context Dump Ends =====')); return message; }; @@ -171,9 +197,9 @@ class NunjucksError extends Error { /** * Provide context for Nunjucks error - * @param {Error} err Nunjucks error - * @param {string} str string input for Nunjucks - * @return {Error} New error object with embedded context + * @param err Nunjucks error + * @param input string input for Nunjucks + * @return New error object with embedded context */ const formatNunjucksError = (err: Error, input: string, source = ''): Error => { err.message = err.message.replace('(unknown path)', source ? magenta(source) : ''); @@ -198,6 +224,10 @@ const formatNunjucksError = (err: Error, input: string, source = ''): Error => { type RegisterOptions = { async?: boolean; ends?: boolean; +}; + +interface RegisterAsyncOptions extends RegisterOptions { + async: boolean; } class Tag { @@ -210,10 +240,76 @@ class Tag { }); } - register(name: string, fn: TagFunction): void - register(name: string, fn: TagFunction, ends: boolean): void - register(name: string, fn: TagFunction, options: RegisterOptions): void - register(name: string, fn: TagFunction, options?: RegisterOptions | boolean):void { + /** + * register shortcode tag + * @param name shortcode tag name + * @param fn shortcode tag function + */ + register(name: string, fn: TagFunction): void; + + /** + * register shortcode tag with RegisterOptions.ends boolean directly + * @param name shortcode tag name + * @param fn callback shortcode tag + * @param ends use endblock + */ + register(name: string, fn: TagFunction, ends: boolean): void; + + /** + * register shortcode tag with synchronous function callback + * @param name shortcode tag name + * @param fn synchronous function callback + * @param options register options + */ + register(name: string, fn: TagFunction, options: RegisterOptions): void; + + /** + * register shortcode tag with synchronous function callback + * @param name shortcode tag name + * @param fn synchronous function callback + * @param options register options + */ + register(name: string, fn: TagFunction, options: { async: false; ends?: boolean }): void; + + /** + * register shortcode tag with asynchronous function callback + * @param name shortcode tag name + * @param fn asynchronous function callback + * @param options register options + */ + register( + name: string, + fn: AsyncTagFunction, + options: { async: true; ends?: boolean } | { async: true; ends: boolean } + ): void; + + /** + * register shortcode tag + * @param name shortcode tag name + * @param fn asynchronous or synchronous function callback + * @param ends add support for end tag + * @example + * without ends + * ```html + * {% youtube video_id [type] [cookie] %} + * ``` + * + * using ends + * ```nunjucks + * {% blockquote [author[, source]] [link] [source_link_title] %} + * content + * {% endblockquote %} + * ``` + */ + register(name: string, fn: TagFunction, ends: boolean): void; + + /** + * register shortcode tag + * @param name shortcode tag name + * @param fn asynchronous or synchronous function callback + * @param options register options + */ + register(name: string, fn: TagFunction | AsyncTagFunction, options?: RegisterOptions | RegisterAsyncOptions | boolean): void { if (!name) throw new TypeError('name is required'); if (typeof fn !== 'function') throw new TypeError('fn must be a function'); @@ -245,6 +341,10 @@ class Tag { this.env.addExtension(name, tag); } + /** + * unregister shortcode tag + * @param name shortcode tag name + */ unregister(name: string): void { if (!name) throw new TypeError('name is required'); @@ -253,10 +353,9 @@ class Tag { if (env.hasExtension(name)) env.removeExtension(name); } - render(str: string): Promise; - render(str: string, callback: NodeJSLikeCallback): Promise; - render(str: string, options: { source?: string, [key: string]: any }, callback?: NodeJSLikeCallback): Promise; - render(str: string, options: { source?: string, [key: string]: any } | NodeJSLikeCallback = {}, callback?: NodeJSLikeCallback): Promise { + render(str: string, options: { source?: string }): Promise; + render(str: string, callback: NodeJSLikeCallback | NodeJSLikeCallback): Promise; + render(str: string, options: { source?: string, [key: string]: any } | NodeJSLikeCallback | NodeJSLikeCallback = {}, callback?: NodeJSLikeCallback | NodeJSLikeCallback): Promise { if (!callback && typeof options === 'function') { callback = options; options = {}; @@ -276,9 +375,10 @@ class Tag { options, cb ); - }).catch(err => { - return Promise.reject(formatNunjucksError(err, str, source)); }) + .catch(err => { + return Promise.reject(formatNunjucksError(err, str, source)); + }) .asCallback(callback); } } diff --git a/lib/hexo/index.ts b/lib/hexo/index.ts index c5e48f8723..4f0edfc2a2 100644 --- a/lib/hexo/index.ts +++ b/lib/hexo/index.ts @@ -464,6 +464,14 @@ class Hexo extends EventEmitter { } } + /** + * load installed plugin + * @param path absolute path to plugin directory + * @param callback + * @returns + * @example + * hexo.loadPlugin(require.resolve('hexo-renderer-marked')); + */ loadPlugin(path: string, callback?: NodeJSLikeCallback): Promise { return readFile(path).then(script => { // Based on: https://github.com/nodejs/node-v0.x-archive/blob/v0.10.33/src/node.js#L516 diff --git a/lib/hexo/post.ts b/lib/hexo/post.ts index 790a540663..8dd61f92e6 100644 --- a/lib/hexo/post.ts +++ b/lib/hexo/post.ts @@ -362,7 +362,7 @@ class Post { return readFile(src); }).then((content: string) => { // Create post - Object.assign(data, yfmParse(content)); + Object.assign(data, yfmParse(String(content))); data.content = data._content; data._content = undefined; diff --git a/lib/plugins/filter/before_post_render/dataType.ts b/lib/plugins/filter/before_post_render/dataType.ts new file mode 100644 index 0000000000..626e6404ce --- /dev/null +++ b/lib/plugins/filter/before_post_render/dataType.ts @@ -0,0 +1,14 @@ +import Hexo from '../../../hexo'; + +/** + * before_post_render `data` parameter + * + * @example + * hexo.extend.filter.register('before_post_render', function(data){ console.log(data.content) }) + */ +export interface extend_filter_before_post_render_data { + [key: string]: any; + content: string | null | undefined; + source: string; + config: Hexo['config']; +} diff --git a/lib/plugins/filter/template_locals/i18n.ts b/lib/plugins/filter/template_locals/i18n.ts index 87c505280b..76feaa2c43 100644 --- a/lib/plugins/filter/template_locals/i18n.ts +++ b/lib/plugins/filter/template_locals/i18n.ts @@ -13,9 +13,17 @@ function i18nLocalsFilter(this: Hexo, locals: LocalsType): void { if (!lang) { const pattern = new Pattern(`${i18nDir}/*path`); - const data = pattern.match(locals.path); + // fix hexo-util/dist/pattern.d.ts is not object + // fix(TS2322): Type 'unknown' is not assignable to type 'string'. + const data = pattern.match(locals.path) as Record; - if (data && data.lang && i18nLanguages.includes(data.lang)) { + if ( + typeof data !== 'undefined' + && !Array.isArray(data) + && typeof data !== 'boolean' + && data.lang + && i18nLanguages.includes(data.lang) + ) { lang = data.lang; page.canonical_path = data.path; } else { diff --git a/package.json b/package.json index 0150fb28b1..f88e6d79e6 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,7 @@ "version": "7.1.1", "description": "A fast, simple & powerful blog framework, powered by Node.js.", "main": "dist/hexo", - "bin": { - "hexo": "./bin/hexo" - }, + "bin": "./bin/hexo", "scripts": { "prepublishOnly": "npm install && npm run clean && npm run build", "build": "tsc -b", @@ -66,13 +64,14 @@ "warehouse": "^5.0.1" }, "devDependencies": { + "0x": "^5.1.2", "@types/abbrev": "^1.1.3", "@types/bluebird": "^3.5.37", + "@types/connect": "^3.4.38", "@types/js-yaml": "^4.0.9", "@types/node": "^18.11.8 <18.19.9", "@types/nunjucks": "^3.2.2", "@types/text-table": "^0.2.4", - "0x": "^5.1.2", "c8": "^9.0.0", "chai": "^4.3.6", "cheerio": "0.22.0",