From ead01d86da847fbaf7f10d15f233e8bf240cb806 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Sat, 8 Aug 2026 22:36:04 +0800 Subject: [PATCH] fix: migrate YAML parsing to yaml --- lib/hexo/multi_config_path.ts | 9 ++--- lib/hexo/post.ts | 4 +-- lib/hexo/yaml.ts | 22 ++++++++++++ lib/plugins/console/config.ts | 4 +-- lib/plugins/renderer/yaml.ts | 47 +++++++++++++++++++------- package-lock.json | 33 ++++-------------- package.json | 6 ++-- test/scripts/box/file.ts | 4 +-- test/scripts/console/config.ts | 4 +-- test/scripts/hexo/multi_config_path.ts | 14 ++++---- test/scripts/hexo/render.ts | 4 +-- test/scripts/renderers/yaml.ts | 36 ++++++++++++++++++++ 12 files changed, 122 insertions(+), 65 deletions(-) create mode 100644 lib/hexo/yaml.ts diff --git a/lib/hexo/multi_config_path.ts b/lib/hexo/multi_config_path.ts index cb81a9539..4733d2cd0 100644 --- a/lib/hexo/multi_config_path.ts +++ b/lib/hexo/multi_config_path.ts @@ -1,8 +1,9 @@ import { isAbsolute, resolve, join, extname } from 'path'; import { existsSync, readFileSync, writeFileSync } from 'hexo-fs'; -import yml from 'js-yaml'; +import { stringify } from 'yaml'; import { deepMerge } from 'hexo-util'; import type Hexo from './index'; +import parseYaml from './yaml'; export = (ctx: Hexo) => function multiConfigPath(base: string, configPaths?: string, outputDir?: string): string { const { log } = ctx; @@ -47,10 +48,10 @@ export = (ctx: Hexo) => function multiConfigPath(base: string, configPaths?: str const ext = extname(paths[i]).toLowerCase(); if (ext === '.yml') { - combinedConfig = deepMerge(combinedConfig, yml.load(file)); + combinedConfig = deepMerge(combinedConfig, parseYaml(file)); count++; } else if (ext === '.json') { - combinedConfig = deepMerge(combinedConfig, yml.load(file, {json: true})); + combinedConfig = deepMerge(combinedConfig, parseYaml(file, { uniqueKeys: false })); count++; } else { log.w(`Config file ${paths[i]} not supported type.`); @@ -69,7 +70,7 @@ export = (ctx: Hexo) => function multiConfigPath(base: string, configPaths?: str log.d(`Writing _multiconfig.yml to ${outputPath}`); - writeFileSync(outputPath, yml.dump(combinedConfig)); + writeFileSync(outputPath, stringify(combinedConfig)); // write file and return path return outputPath; diff --git a/lib/hexo/post.ts b/lib/hexo/post.ts index 9418039e4..5faa33dce 100644 --- a/lib/hexo/post.ts +++ b/lib/hexo/post.ts @@ -3,12 +3,12 @@ import moment from 'moment'; import Promise from 'bluebird'; import { join, extname, basename } from 'path'; import { magenta } from 'picocolors'; -import { load } from 'js-yaml'; import { slugize, escapeRegExp, deepMerge} from 'hexo-util'; import { copyDir, exists, listDir, mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs'; import { parse as yfmParse, split as yfmSplit, stringify as yfmStringify } from 'hexo-front-matter'; import type Hexo from './index'; import type { NodeJSLikeCallback, RenderData } from '../types'; +import parseYaml from './yaml'; const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content']; @@ -454,7 +454,7 @@ class Post { const jsonMode = separator.startsWith(';'); // Parse front-matter - let obj = jsonMode ? JSON.parse(`{${frontMatter}}`) : load(frontMatter); + let obj = jsonMode ? JSON.parse(`{${frontMatter}}`) : parseYaml(frontMatter); obj = deepMerge(obj, Object.fromEntries(Object.entries(data).filter(([key, value]) => !preservedKeys.includes(key) && value != null))); diff --git a/lib/hexo/yaml.ts b/lib/hexo/yaml.ts new file mode 100644 index 000000000..d4df02be1 --- /dev/null +++ b/lib/hexo/yaml.ts @@ -0,0 +1,22 @@ +import { parseDocument, type ParseOptions, type Tags } from 'yaml'; + +interface ParseYamlOptions { + customTags?: Tags; + uniqueKeys?: ParseOptions['uniqueKeys']; +} + +export default function parseYaml(source: string, options: ParseYamlOptions = {}): any { + const customTags: Tags = options.customTags ? ['timestamp', ...options.customTags] : ['timestamp']; + const document = parseDocument(source, { + customTags, + merge: true, + uniqueKeys: options.uniqueKeys + }); + + if (document.errors.length > 0) throw document.errors[0]; + + const unresolvedTag = document.warnings.find(warning => warning.code === 'TAG_RESOLVE_FAILED'); + if (unresolvedTag) throw unresolvedTag; + + return document.toJS(); +} diff --git a/lib/plugins/console/config.ts b/lib/plugins/console/config.ts index c46f9793f..37ffb303b 100644 --- a/lib/plugins/console/config.ts +++ b/lib/plugins/console/config.ts @@ -1,4 +1,4 @@ -import yaml from 'js-yaml'; +import { stringify } from 'yaml'; import { exists, writeFile } from 'hexo-fs'; import { extname } from 'path'; import Promise from 'bluebird'; @@ -35,7 +35,7 @@ function configConsole(this: Hexo, args: ConfigArgs): Promise { setProperty(config, key, castValue(value)); - const result = ext === '.json' ? JSON.stringify(config) : yaml.dump(config); + const result = ext === '.json' ? JSON.stringify(config) : stringify(config); return writeFile(configPath, result); }); diff --git a/lib/plugins/renderer/yaml.ts b/lib/plugins/renderer/yaml.ts index ceef7f7a1..43a40270a 100644 --- a/lib/plugins/renderer/yaml.ts +++ b/lib/plugins/renderer/yaml.ts @@ -1,22 +1,43 @@ -import yaml from 'js-yaml'; import { escape } from 'hexo-front-matter'; -import logger from 'hexo-log'; +import type { ScalarTag } from 'yaml'; import type { StoreFunctionData } from '../../extend/renderer'; +import parseYaml from '../../hexo/yaml'; -let schema: yaml.Schema; -// FIXME: workaround for https://github.com/hexojs/hexo/issues/4917 -try { - schema = yaml.DEFAULT_SCHEMA.extend(require('js-yaml-js-types').all); -} catch (e) { - if (e instanceof yaml.YAMLException) { - logger().warn('YAMLException: please see https://github.com/hexojs/hexo/issues/4917'); - } else { - throw e; +const jsRegexp: ScalarTag = { + identify: value => value instanceof RegExp, + tag: 'tag:yaml.org,2002:js/regexp', + resolve(value, onError) { + if (!value) { + onError('Invalid RegExp value'); + return value; + } + + let regexp = value; + let modifiers = ''; + + if (regexp[0] === '/') { + const tail = /\/([gim]*)$/.exec(regexp); + if (tail) modifiers = tail[1]; + + if (regexp[regexp.length - modifiers.length - 1] !== '/') { + onError('Invalid RegExp value'); + return value; + } + + regexp = regexp.slice(1, regexp.length - modifiers.length - 1); + } + + try { + return new RegExp(regexp, modifiers); + } catch (error) { + onError(error instanceof Error ? error.message : 'Invalid RegExp value'); + return value; + } } -} +}; function yamlHelper(data: StoreFunctionData): any { - return yaml.load(escape(data.text), { schema }); + return parseYaml(escape(data.text), { customTags: [jsRegexp] }); } export = yamlHelper; diff --git a/package-lock.json b/package-lock.json index 49711350d..04c685f0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,6 @@ "hexo-i18n": "^2.0.0", "hexo-log": "^4.1.0", "hexo-util": "^4.0.0", - "js-yaml": "^4.1.0", - "js-yaml-js-types": "^1.0.1", "micromatch": "^4.0.8", "moize": "^6.1.6", "moment": "^2.30.1", @@ -31,7 +29,8 @@ "strip-ansi": "^7.1.0", "tildify": "^3.0.0", "titlecase": "^1.1.3", - "warehouse": "^6.0.0" + "warehouse": "^6.0.0", + "yaml": "^2.9.0" }, "bin": { "hexo": "bin/hexo" @@ -41,7 +40,6 @@ "@types/bluebird": "^3.5.37", "@types/chai": "^4.3.11", "@types/graceful-fs": "^4.1.9", - "@types/js-yaml": "^4.0.9", "@types/micromatch": "^4.0.7", "@types/mocha": "^10.0.9", "@types/node": "^20.17.6", @@ -487,13 +485,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3342,6 +3333,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -4911,18 +4903,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/js-yaml-js-types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-yaml-js-types/-/js-yaml-js-types-1.0.1.tgz", - "integrity": "sha512-5tpfyORs8OQ43alNERbWfYRCtWgykvzYgY46fUhrQi2+kS7N0NuuFYLZ/IrfmVm5muLTndeMublgraXiFRjEPw==", - "license": "MIT", - "dependencies": { - "esprima": "^4.0.1" - }, - "peerDependencies": { - "js-yaml": "4.x" - } - }, "node_modules/jsdom": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", @@ -8551,10 +8531,9 @@ } }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 75ae89c19..fe55f5255 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,6 @@ "hexo-i18n": "^2.0.0", "hexo-log": "^4.1.0", "hexo-util": "^4.0.0", - "js-yaml": "^4.1.0", - "js-yaml-js-types": "^1.0.1", "micromatch": "^4.0.8", "moize": "^6.1.6", "moment": "^2.30.1", @@ -64,14 +62,14 @@ "strip-ansi": "^7.1.0", "tildify": "^3.0.0", "titlecase": "^1.1.3", - "warehouse": "^6.0.0" + "warehouse": "^6.0.0", + "yaml": "^2.9.0" }, "devDependencies": { "@types/abbrev": "^1.1.3", "@types/bluebird": "^3.5.37", "@types/chai": "^4.3.11", "@types/graceful-fs": "^4.1.9", - "@types/js-yaml": "^4.0.9", "@types/micromatch": "^4.0.7", "@types/mocha": "^10.0.9", "@types/node": "^20.17.6", diff --git a/test/scripts/box/file.ts b/test/scripts/box/file.ts index 3e3f65b5c..259f77134 100644 --- a/test/scripts/box/file.ts +++ b/test/scripts/box/file.ts @@ -1,6 +1,6 @@ import { join } from 'path'; import { rmdir, stat, statSync, writeFile } from 'hexo-fs'; -import { load } from 'js-yaml'; +import { parse } from 'yaml'; import Hexo from '../../../lib/hexo'; import Box from '../../../lib/box'; @@ -21,7 +21,7 @@ describe('File', () => { '- Banana' ].join('\n'); - const obj = load(body); + const obj = parse(body); const path = 'test.yml'; const makeFile = (path, props) => { diff --git a/test/scripts/console/config.ts b/test/scripts/console/config.ts index 8dc26b75c..584d854b3 100644 --- a/test/scripts/console/config.ts +++ b/test/scripts/console/config.ts @@ -1,6 +1,6 @@ import { mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs'; import { join } from 'path'; -import { load } from 'js-yaml'; +import { parse } from 'yaml'; import { stub, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import configConsole from '../../../lib/plugins/console/config'; @@ -65,7 +65,7 @@ describe('config', () => { async function writeConfig(...args) { await config({_: args}); const content = await readFile(hexo.config_path); - return load(content) as any; + return parse(content) as any; } it('write config', async () => { diff --git a/test/scripts/hexo/multi_config_path.ts b/test/scripts/hexo/multi_config_path.ts index dd7ad4202..c78a4449c 100644 --- a/test/scripts/hexo/multi_config_path.ts +++ b/test/scripts/hexo/multi_config_path.ts @@ -1,7 +1,7 @@ import pathFn from 'path'; import osFn from 'os'; import { writeFileSync, rmdirSync, unlinkSync, readFileSync } from 'hexo-fs'; -import yml from 'js-yaml'; +import { parse } from 'yaml'; import Hexo from '../../../lib/hexo'; import multiConfigPath from '../../../lib/hexo/multi_config_path'; @@ -215,14 +215,14 @@ describe('config flag handling', () => { it('2 YAML overwrite', () => { const configFile = mcp(base, 'test1.yml,test2.yml'); let config: any = readFileSync(configFile); - config = yml.load(config); + config = parse(config); config.author.should.eql('bar'); config.favorites.food.should.eql('candy'); config.type.should.eql('dinosaur'); config = readFileSync(mcp(base, 'test2.yml,test1.yml')); - config = yml.load(config); + config = parse(config); config.author.should.eql('foo'); config.favorites.food.should.eql('sushi'); @@ -231,14 +231,14 @@ describe('config flag handling', () => { it('2 JSON overwrite', () => { let config: any = readFileSync(mcp(base, 'test1.json,test2.json')); - config = yml.load(config); + config = parse(config); config.author.should.eql('waldo'); config.favorites.food.should.eql('ice cream'); config.type.should.eql('elephant'); config = readFileSync(mcp(base, 'test2.json,test1.json')); - config = yml.load(config); + config = parse(config); config.author.should.eql('dinosaur'); config.favorites.food.should.eql('burgers'); @@ -247,14 +247,14 @@ describe('config flag handling', () => { it('JSON & YAML overwrite', () => { let config: any = readFileSync(mcp(base, 'test1.yml,test1.json')); - config = yml.load(config); + config = parse(config); config.author.should.eql('dinosaur'); config.favorites.food.should.eql('burgers'); config.type.should.eql('elephant'); config = readFileSync(mcp(base, 'test1.json,test1.yml')); - config = yml.load(config); + config = parse(config); config.author.should.eql('foo'); config.favorites.food.should.eql('sushi'); diff --git a/test/scripts/hexo/render.ts b/test/scripts/hexo/render.ts index 01e9cb263..f9f65c9cd 100644 --- a/test/scripts/hexo/render.ts +++ b/test/scripts/hexo/render.ts @@ -1,6 +1,6 @@ import { writeFile, rmdir } from 'hexo-fs'; import { join } from 'path'; -import yaml from 'js-yaml'; +import { parse } from 'yaml'; import { spy, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; @@ -23,7 +23,7 @@ describe('Render', () => { '- Banana' ].join('\n'); - const obj = yaml.load(body); + const obj = parse(body); const path = join(hexo.base_dir, 'test.yml'); before(async () => { diff --git a/test/scripts/renderers/yaml.ts b/test/scripts/renderers/yaml.ts index 2cd7934cf..07264f4c5 100644 --- a/test/scripts/renderers/yaml.ts +++ b/test/scripts/renderers/yaml.ts @@ -1,4 +1,6 @@ import r from '../../../lib/plugins/renderer/yaml'; +import chai from 'chai'; +const should = chai.should(); describe('yaml', () => { it('normal', () => { @@ -19,4 +21,38 @@ describe('yaml', () => { } }); }); + + it('supports merge keys', () => { + const body = [ + 'defaults: &defaults', + ' foo: 1', + 'bar:', + ' <<: *defaults', + ' baz: 2' + ].join('\n'); + + r({text: body}).should.eql({ + defaults: {foo: 1}, + bar: {foo: 1, baz: 2} + }); + }); + + it('parses timestamps as dates', () => { + r({text: 'date: 2026-08-08'}).date.should.eql(new Date('2026-08-08')); + }); + + it('supports !!js/regexp', () => { + const result = r({text: 'pattern: !!js/regexp /foo/gim'}); + + result.pattern.should.be.instanceOf(RegExp); + result.pattern.source.should.eql('foo'); + result.pattern.flags.should.eql('gim'); + }); + + it('rejects !!js/function', () => { + should.throw( + () => r({text: 'fn: !!js/function function () { return 1; }'}), + /Unresolved tag: tag:yaml.org,2002:js\/function/ + ); + }); });