diff --git a/.appveyor.yml b/.appveyor.yml index 855831e38..b27085de3 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -33,7 +33,7 @@ for: - ps: Install-Product node $env:nodejs_version $env:platform - npm -g i npm@latest build_script: - - appveyor-retry call npm install --build-from-source + - set npm_config_build_from_source=true&& appveyor-retry call npm install test_script: - npm test after_test: diff --git a/node/README.md b/node/README.md index 7ded72021..5530742e3 100644 --- a/node/README.md +++ b/node/README.md @@ -77,13 +77,23 @@ TypeScript type declarations are included. Creates a converter. If `config` is omitted, OpenCC uses `s2t.json`. `config` can be one of the built-in configuration file names, such as -`s2t.json`, or an absolute path to a custom OpenCC configuration file. +`s2t.json`, an absolute path to a custom OpenCC configuration file, or an +OpenCC configuration object. `options.includeTofuRiskDictionaries` controls whether dictionaries marked as possibly outputting tofu are loaded. Here, tofu means Chinese characters that cannot be displayed and may render as missing-glyph boxes. It defaults to `true` for JavaScript API compatibility. +`options.configDirectory` sets the base directory used to resolve relative +dictionary paths in inline configuration objects. It defaults to the package's +bundled OpenCC assets directory. + +### `OpenCC.fromConfig(config, options?)` + +Creates a converter from an OpenCC configuration object. This is equivalent to +passing the object to `new OpenCC(config, options)`. + ### `converter.convertSync(input)` Converts text synchronously and returns the converted string. @@ -188,6 +198,34 @@ Relative config paths are resolved from the current working directory by the CLI. In the JavaScript API, relative config names are resolved from the package assets directory, so custom configs should be passed as absolute paths. +You can also pass an OpenCC configuration object directly: + +```js +import OpenCC from 'opencc'; + +const converter = OpenCC.fromConfig({ + name: 'Custom Inline Config', + segmentation: { + type: 'mmseg', + dict: { type: 'inline', entries: { '鼠标': '鼠标' } }, + }, + conversion_chain: [{ + dict: { type: 'inline', entries: { '鼠标': '滑鼠' } }, + }], +}); + +console.log(converter.convertSync('鼠标坏了')); +``` + +When an inline configuration references relative dictionary files, pass +`configDirectory`: + +```js +const converter = OpenCC.fromConfig(config, { + configDirectory: '/absolute/path/to/opencc-resources', +}); +``` + Custom dictionaries can be generated with `OpenCC.generateDict()` or with the native `opencc_dict` tool from the full OpenCC distribution. diff --git a/node/README.zh.md b/node/README.zh.md index 70aaefaee..e80e83c30 100644 --- a/node/README.zh.md +++ b/node/README.zh.md @@ -75,12 +75,20 @@ converter.convert('漢字', (err, text) => { 建立轉換器。若省略 `config`,OpenCC 會使用 `s2t.json`。 `config` 可以是內建配置檔名稱,例如 `s2t.json`,也可以是自訂 -OpenCC 配置檔的絕對路徑。 +OpenCC 配置檔的絕對路徑,或 OpenCC 配置物件。 `options.includeTofuRiskDictionaries` 控制是否載入標記為可能輸出 tofu 的字典。這裡的 tofu 指無法顯示的中文字,有時會被渲染為方塊,俗稱 豆腐塊。為維持 JavaScript API 相容性,預設值為 `true`。 +`options.configDirectory` 設定 inline 配置物件中相對字典路徑的基準 +目錄。預設會使用套件內建的 OpenCC assets 目錄。 + +### `OpenCC.fromConfig(config, options?)` + +從 OpenCC 配置物件建立轉換器。這等同於將該物件傳給 +`new OpenCC(config, options)`。 + ### `converter.convertSync(input)` 同步轉換文字,並回傳轉換後的字串。 @@ -183,6 +191,33 @@ console.log(converter.convertSync('汉字')); CLI 的相對配置路徑會從目前工作目錄解析。在 JavaScript API 中,相對 配置名稱會從套件 assets 目錄解析,因此自訂配置應使用絕對路徑傳入。 +也可以直接傳入 OpenCC 配置物件: + +```js +import OpenCC from 'opencc'; + +const converter = OpenCC.fromConfig({ + name: 'Custom Inline Config', + segmentation: { + type: 'mmseg', + dict: { type: 'inline', entries: { '鼠标': '鼠标' } }, + }, + conversion_chain: [{ + dict: { type: 'inline', entries: { '鼠标': '滑鼠' } }, + }], +}); + +console.log(converter.convertSync('鼠标坏了')); +``` + +若 inline 配置引用相對字典檔,請傳入 `configDirectory`: + +```js +const converter = OpenCC.fromConfig(config, { + configDirectory: '/absolute/path/to/opencc-resources', +}); +``` + 自訂字典可以透過 `OpenCC.generateDict()` 產生,也可以使用完整 OpenCC 發行版中的原生 `opencc_dict` 工具。 diff --git a/node/opencc.d.cts b/node/opencc.d.cts index a0e875ecf..ee28b3c21 100644 --- a/node/opencc.d.cts +++ b/node/opencc.d.cts @@ -1,11 +1,15 @@ +type OpenCCConfig = Record; + interface OpenCCOptions { includeTofuRiskDictionaries?: boolean; + configDirectory?: string; resourceZip?: string; } declare class OpenCC { - constructor(config?: string, options?: OpenCCOptions); + constructor(config?: string | OpenCCConfig, options?: OpenCCOptions); static readonly version: string; + static fromConfig(config: OpenCCConfig, options?: OpenCCOptions): OpenCC; static generateDict(inputFileName: string, outputFileName: string, formatFrom: string, formatTo: string): void; convert(input: string, callback: (err: string | undefined, convertedText: string) => void): void; convertSync(input: string): string; @@ -13,7 +17,7 @@ declare class OpenCC { } declare namespace OpenCC { - export { OpenCC, OpenCCOptions }; + export { OpenCC, OpenCCConfig, OpenCCOptions }; } export = OpenCC; diff --git a/node/opencc.d.mts b/node/opencc.d.mts index 17f4d5c7e..e9b880f77 100644 --- a/node/opencc.d.mts +++ b/node/opencc.d.mts @@ -1,15 +1,19 @@ +type OpenCCConfig = Record; + interface OpenCCOptions { includeTofuRiskDictionaries?: boolean; + configDirectory?: string; resourceZip?: string; } declare class OpenCC { - constructor(config?: string, options?: OpenCCOptions); + constructor(config?: string | OpenCCConfig, options?: OpenCCOptions); static readonly version: string; + static fromConfig(config: OpenCCConfig, options?: OpenCCOptions): OpenCC; static generateDict(inputFileName: string, outputFileName: string, formatFrom: string, formatTo: string): void; convert(input: string, callback: (err: string | undefined, convertedText: string) => void): void; convertSync(input: string): string; convertPromise(input: string): Promise; } -export { OpenCC, OpenCCOptions }; +export { OpenCC, OpenCCConfig, OpenCCOptions }; export default OpenCC; diff --git a/node/opencc.d.ts b/node/opencc.d.ts index 07a106e46..ec4c12ca8 100644 --- a/node/opencc.d.ts +++ b/node/opencc.d.ts @@ -1,14 +1,18 @@ +type OpenCCConfig = Record; + interface OpenCCOptions { includeTofuRiskDictionaries?: boolean; + configDirectory?: string; resourceZip?: string; } declare class OpenCC { - constructor(config?: string, options?: OpenCCOptions); + constructor(config?: string | OpenCCConfig, options?: OpenCCOptions); static readonly version: string; + static fromConfig(config: OpenCCConfig, options?: OpenCCOptions): OpenCC; static generateDict(inputFileName: string, outputFileName: string, formatFrom: string, formatTo: string): void; convert(input: string, callback: (err: string | undefined, convertedText: string) => void): void; convertSync(input: string): string; convertPromise(input: string): Promise; } -export { OpenCC, OpenCCOptions }; +export { OpenCC, OpenCCConfig, OpenCCOptions }; diff --git a/node/opencc.js b/node/opencc.js index b82cce452..1a797a27b 100644 --- a/node/opencc.js +++ b/node/opencc.js @@ -143,6 +143,9 @@ function patchDictPaths(dict, baseDir) { function filterTofuRiskDicts(dict, includeTofuRiskDictionaries) { if (!dict) return null; + if (dict.type === 'inline') { + return dict; + } if (dict.may_output_tofu && !includeTofuRiskDictionaries) { return null; } @@ -168,6 +171,34 @@ function filterTofuRiskConversionChain(config, includeTofuRiskDictionaries) { .filter(Boolean); } +function isConfigObject(config) { + return config && typeof config === 'object' && !Array.isArray(config); +} + +function cloneConfigObject(config) { + return JSON.parse(JSON.stringify(config)); +} + +function createFromConfigObject(config, options) { + if (!isConfigObject(config)) { + throw new TypeError('OpenCC config must be an object'); + } + if (options.resourceZip) { + throw new TypeError('resourceZip is only supported with config file names'); + } + + const raw = cloneConfigObject(config); + const includeTofuRiskDictionaries = + options.includeTofuRiskDictionaries !== false; + filterTofuRiskConversionChain(raw, includeTofuRiskDictionaries); + + const configDirectory = options.configDirectory || assetsPath; + return new binding.Opencc( + JSON.stringify(raw), + configDirectory + (/[\\/]$/.test(configDirectory) ? '' : path.sep) + ); +} + /** * Patch all relative paths in a config JSON object to absolute paths. * @@ -242,6 +273,12 @@ const OpenCC = module.exports = function (config, options) { if (!options) { options = {}; } + + if (isConfigObject(config)) { + this.handler = createFromConfigObject(config, options); + return; + } + const includeTofuRiskDictionaries = options.includeTofuRiskDictionaries !== false; @@ -315,6 +352,10 @@ OpenCC._assetsPath = assetsPath; */ OpenCC.version = binding.Opencc.version(); +OpenCC.fromConfig = function (config, options) { + return new OpenCC(config, options); +}; + /** * Generates dictionary from another format. * diff --git a/node/test.js b/node/test.js index c0fa22b5f..65fdea37d 100644 --- a/node/test.js +++ b/node/test.js @@ -79,6 +79,123 @@ describe('API compatibility', function () { assert.equal(opencc.convertSync('㑮'), '𫝈'); }); + it('supports inline configuration objects', function () { + const opencc = new OpenCC({ + name: 'Inline Object Config', + segmentation: { + type: 'mmseg', + dict: { + type: 'inline', + entries: { + '鼠标': '鼠标', + }, + }, + }, + conversion_chain: [{ + dict: { + type: 'inline', + entries: { + '鼠标': '滑鼠', + }, + }, + }], + }); + + assert.equal(opencc.convertSync('鼠标坏了'), '滑鼠坏了'); + }); + + it('uses bundled assets as the default inline configuration directory', function () { + const opencc = OpenCC.fromConfig({ + name: 'Inline Object Config With Bundled Assets', + conversion_chain: [{ + dict: { type: 'ocd2', file: 'STCharacters.ocd2' }, + }], + }); + + assert.equal(opencc.convertSync('汉字'), '漢字'); + }); + + it('supports inline configuration objects with relative dictionary files', function () { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencc-node-inline-config-')); + const dict = path.join(dir, 'phrases.txt'); + fs.writeFileSync(dict, '鼠标\t滑鼠\n', 'utf8'); + + try { + const opencc = new OpenCC({ + name: 'Inline Object Config With Files', + segmentation: { + type: 'mmseg', + dict: { type: 'text', file: 'phrases.txt' }, + }, + conversion_chain: [{ + dict: { type: 'text', file: 'phrases.txt' }, + }], + }, { configDirectory: dir }); + + assert.equal(opencc.convertSync('鼠标坏了'), '滑鼠坏了'); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('supports OpenCC.fromConfig with group tofu-risk dictionary filtering', function () { + const config = { + name: 'Inline Object Config Tofu Risk', + conversion_chain: [ + { + dict: { + type: 'group', + may_output_tofu: true, + match_policy: 'short_circuit', + dicts: [{ + type: 'inline', + entries: { + 'A': 'B', + }, + }], + }, + }, + { + dict: { + type: 'inline', + entries: { + 'A': 'C', + }, + }, + }, + ], + }; + + const opencc = OpenCC.fromConfig(config, { includeTofuRiskDictionaries: false }); + assert.equal(opencc.convertSync('A'), 'C'); + assert.equal(config.conversion_chain.length, 2); + }); + + it('rejects inline tofu-risk dictionaries consistently with C++ config parsing', function () { + const result = childProcess.spawnSync(process.execPath, ['-e', ` + const assert = require('assert'); + const OpenCC = require('./node/opencc'); + assert.throws(function () { + OpenCC.fromConfig({ + name: 'Inline Object Config Invalid Tofu Risk', + conversion_chain: [{ + dict: { + type: 'inline', + may_output_tofu: true, + entries: { + 'A': 'B', + }, + }, + }], + }, { includeTofuRiskDictionaries: false }); + }, /Inline dictionary does not support may_output_tofu/); + `], { + cwd: path.resolve(__dirname, '..'), + encoding: 'utf8', + }); + assert.equal(result.status, 0, result.stderr); + }); + it('supports JSONC (JSON with comments and trailing commas) configuration files', function () { const tempConfigPath = path.join(os.tmpdir(), 'test_comment_config.json'); fs.writeFileSync(tempConfigPath, `