Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 39 additions & 1 deletion node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
37 changes: 36 additions & 1 deletion node/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`

同步轉換文字,並回傳轉換後的字串。
Expand Down Expand Up @@ -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` 工具。

Expand Down
8 changes: 6 additions & 2 deletions node/opencc.d.cts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
type OpenCCConfig = Record<string, unknown>;

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<string>;
}

declare namespace OpenCC {
export { OpenCC, OpenCCOptions };
export { OpenCC, OpenCCConfig, OpenCCOptions };
}

export = OpenCC;
8 changes: 6 additions & 2 deletions node/opencc.d.mts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
type OpenCCConfig = Record<string, unknown>;

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<string>;
}
export { OpenCC, OpenCCOptions };
export { OpenCC, OpenCCConfig, OpenCCOptions };
export default OpenCC;
8 changes: 6 additions & 2 deletions node/opencc.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
type OpenCCConfig = Record<string, unknown>;

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<string>;
}
export { OpenCC, OpenCCOptions };
export { OpenCC, OpenCCConfig, OpenCCOptions };
41 changes: 41 additions & 0 deletions node/opencc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
Expand Down
117 changes: 117 additions & 0 deletions node/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, `
Expand Down
Loading