;
99 | mainLanguage: Language;
100 | isHeiTypeface: boolean;
101 | showRomanization: ShowRomanization;
102 | showReverseCode: boolean;
103 | }
104 |
105 | export type Preferences = RimePreferences & InterfacePreferences;
106 |
107 | export type PreferencesWithSetter = Preferences & { [P in keyof Preferences as `set${Capitalize}`]: Dispatch> };
108 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | workflow_call:
4 | pull_request:
5 | jobs:
6 | build:
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | os: [ubuntu-latest, windows-latest]
12 | steps:
13 | - name: Checkout Latest Commit
14 | uses: actions/checkout@v4
15 | with:
16 | submodules: recursive
17 | - name: Install Ubuntu Dependencies
18 | if: ${{ matrix.os == 'ubuntu-latest' }}
19 | run: |
20 | sudo apt update
21 | sudo apt upgrade -y
22 | sudo apt install -y \
23 | ninja-build \
24 | libboost-dev \
25 | libboost-regex-dev \
26 | libyaml-cpp-dev \
27 | libleveldb-dev \
28 | libmarisa-dev \
29 | libopencc-dev
30 | echo "CC=/usr/bin/clang" >> $GITHUB_ENV
31 | echo "CXX=/usr/bin/clang++" >> $GITHUB_ENV
32 | - name: Install macOS Dependencies
33 | if: ${{ startsWith(matrix.os, 'macos') }}
34 | run: |
35 | brew install ninja
36 | - name: Install Windows Dependencies
37 | if: ${{ matrix.os == 'windows-latest' }}
38 | run: |
39 | choco upgrade -y llvm
40 | pip install ninja
41 | echo "$env:ProgramFiles\LLVM\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
42 | - name: Setup Bun
43 | uses: oven-sh/setup-bun@v1
44 | with:
45 | bun-version: latest
46 | - name: Setup Emscripten SDK
47 | uses: mymindstorm/setup-emsdk@v14
48 | - name: Install Package Dependencies
49 | run: |
50 | bun i
51 | - name: Prepare Boost
52 | run: |
53 | bun run boost
54 | - name: Build Native
55 | run: |
56 | bun run native
57 | - name: Build Schema
58 | run: |
59 | bun run schema
60 | - name: Build Library
61 | run: |
62 | bun run lib
63 | - name: Build WebAssembly
64 | run: |
65 | bun run wasm
66 | - name: Build App
67 | run: |
68 | bun run build
69 | docker:
70 | runs-on: ubuntu-latest
71 | strategy:
72 | fail-fast: false
73 | steps:
74 | - name: Checkout Latest Commit
75 | uses: actions/checkout@v4
76 | with:
77 | submodules: recursive
78 | - name: Build App
79 | run: |
80 | docker build -t typeduck-web .
81 | docker create --name build typeduck-web
82 | - name: Copy Files
83 | run: |
84 | docker cp build:/TypeDuck-Web/dist ./TypeDuck-Web
85 | - name: Compress Files
86 | run: |
87 | tar -cvf TypeDuck-Web.tar TypeDuck-Web
88 | - name: Upload Artifact
89 | uses: actions/upload-artifact@v4
90 | with:
91 | name: TypeDuck-Web
92 | path: TypeDuck-Web.tar
93 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useReducer, useState } from "react";
2 |
3 | import { ToastContainer } from "react-toastify";
4 |
5 | import CandidatePanel from "./CandidatePanel";
6 | import { NO_AUTO_FILL } from "./consts";
7 | import { useLoading, usePreferences } from "./hooks";
8 | import Preferences from "./Preferences";
9 | import Rime, { subscribe } from "./rime";
10 | import ThemeSwitcher from "./ThemeSwitcher";
11 | import Toolbar from "./Toolbar";
12 | import { notify } from "./utils";
13 |
14 | export default function App() {
15 | const [textArea, setTextArea] = useState(null);
16 | const [loading, runAsyncTask, startAsyncTask] = useLoading();
17 |
18 | useEffect(() => {
19 | const { resolve } = startAsyncTask();
20 | return subscribe("initialized", success => {
21 | if (!success) {
22 | notify("error", "啟動輸入法引擎", "initializing the input method engine");
23 | }
24 | resolve();
25 | });
26 | }, [startAsyncTask]);
27 |
28 | useEffect(() => {
29 | let resolve!: () => void;
30 | let reject!: () => void;
31 | return subscribe("deployStatusChanged", status => {
32 | switch (status) {
33 | case "start":
34 | ({ resolve, reject } = startAsyncTask());
35 | break;
36 | case "success":
37 | resolve();
38 | break;
39 | case "failure":
40 | notify("warning", "重新整理", "refreshing");
41 | reject();
42 | break;
43 | }
44 | });
45 | }, [startAsyncTask]);
46 |
47 | const [deployStatus, updateDeployStatus] = useReducer((n: number) => n + 1, 0);
48 | const preferences = usePreferences();
49 | const { pageSize, enableCompletion, enableCorrection, enableSentence, enableLearning, isCangjie5, isHeiTypeface } = preferences;
50 | useEffect(() =>
51 | runAsyncTask(async () => {
52 | let type: "warning" | "error" | undefined;
53 | try {
54 | const success = await Rime.customize({ pageSize, enableCompletion, enableCorrection, enableSentence, enableLearning, isCangjie5 });
55 | if (!(await Rime.deploy() && success)) {
56 | type = "warning";
57 | }
58 | }
59 | catch {
60 | type = "error";
61 | }
62 | if (type) {
63 | notify(type, "套用設定", "applying the settings");
64 | }
65 | updateDeployStatus();
66 | }), [pageSize, enableCompletion, enableCorrection, enableSentence, enableLearning, isCangjie5, updateDeployStatus, runAsyncTask]);
67 |
68 | return <>
69 |
76 |
77 |
78 |
79 | {textArea && }
80 |
81 |
82 |
90 |
91 | >;
92 | }
93 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "extends": [
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/strict-type-checked",
7 | "plugin:@typescript-eslint/stylistic-type-checked",
8 | "plugin:import/recommended",
9 | "plugin:import/typescript",
10 | "plugin:react/recommended",
11 | "plugin:react-hooks/recommended",
12 | "plugin:jsx-a11y/strict"
13 | ],
14 | "parserOptions": {
15 | "ecmaVersion": "latest",
16 | "project": "tsconfig.json"
17 | },
18 | "plugins": [
19 | "@typescript-eslint",
20 | "import",
21 | "react",
22 | "jsx-a11y",
23 | "react-refresh",
24 | "no-array-concat"
25 | ],
26 | "ignorePatterns": [
27 | "node_modules",
28 | "boost",
29 | "librime",
30 | "schema",
31 | "public",
32 | "build",
33 | "dist"
34 | ],
35 | "settings": {
36 | "import/core-modules": ["bun", "react", "react-dom"]
37 | },
38 | "rules": {
39 | "default-case-last": "error",
40 | "eqeqeq": "error",
41 | "func-style": ["error", "declaration"],
42 | "grouped-accessor-pairs": ["error", "getBeforeSet"],
43 | "logical-assignment-operators": "error",
44 | "no-array-concat/no-array-concat": "error",
45 | "no-constant-binary-expression": "error",
46 | "no-control-regex": "off",
47 | "no-irregular-whitespace": ["error", {
48 | "skipStrings": true,
49 | "skipComments": true,
50 | "skipRegExps": true,
51 | "skipTemplates": true,
52 | "skipJSXText": true
53 | }],
54 | "no-negated-condition": "warn",
55 | "no-self-compare": "error",
56 | "no-template-curly-in-string": "warn",
57 | "no-undef-init": "error",
58 | "no-unmodified-loop-condition": "warn",
59 | "no-unneeded-ternary": "error",
60 | "no-unreachable-loop": "warn",
61 | "no-useless-computed-key": "error",
62 | "no-useless-concat": "error",
63 | "no-useless-return": "error",
64 | "no-var": "error",
65 | "operator-assignment": "error",
66 | "prefer-const": "error",
67 | "prefer-exponentiation-operator": "error",
68 | "prefer-object-spread": "error",
69 | "prefer-regex-literals": "error",
70 | "prefer-rest-params": "error",
71 | "prefer-template": "warn",
72 |
73 | "@typescript-eslint/consistent-type-exports": "error",
74 | "@typescript-eslint/default-param-last": "error",
75 | "@typescript-eslint/no-confusing-void-expression": "off",
76 | "@typescript-eslint/no-loop-func": "warn",
77 | "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
78 | "@typescript-eslint/no-non-null-assertion": "off",
79 | "@typescript-eslint/no-unnecessary-qualifier": "error",
80 | "@typescript-eslint/no-useless-empty-export": "error",
81 | "@typescript-eslint/parameter-properties": ["error", { "prefer": "parameter-property" }],
82 | "@typescript-eslint/prefer-nullish-coalescing": "off",
83 | "@typescript-eslint/prefer-regexp-exec": "error",
84 | "@typescript-eslint/prefer-string-starts-ends-with": "off",
85 | "@typescript-eslint/require-await": "off",
86 | "@typescript-eslint/restrict-plus-operands": ["error", {
87 | "allowAny": true,
88 | "allowBoolean": true,
89 | "allowNumberAndString": true
90 | }],
91 | "@typescript-eslint/restrict-template-expressions": ["error", {
92 | "allowAny": true,
93 | "allowBoolean": true,
94 | "allowNumber": true,
95 | "allowNever": true
96 | }],
97 |
98 | "import/consistent-type-specifier-style": ["error", "prefer-top-level"],
99 | "import/extensions": "error",
100 | "import/first": "error",
101 | "import/newline-after-import": "error",
102 | "import/no-absolute-path": "error",
103 | "import/no-anonymous-default-export": ["warn", { "allowCallExpression": false }],
104 | "import/no-extraneous-dependencies": ["error", {
105 | "devDependencies": ["*.config.ts"],
106 | "includeInternal": true,
107 | "includeTypes": true
108 | }],
109 | "import/no-named-default": "error",
110 | "import/no-self-import": "error",
111 | "import/no-useless-path-segments": ["error", { "noUselessIndex": true }],
112 | "import/order": ["error", {
113 | "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"], "type", "unknown", "object"],
114 | "newlines-between": "always",
115 | "alphabetize": { "order": "asc", "orderImportKind": "asc", "caseInsensitive": true },
116 | "warnOnUnassignedImports": true
117 | }],
118 |
119 | "react/react-in-jsx-scope": "off",
120 | "react/prop-types": "off",
121 |
122 | "react-refresh/only-export-components": ["warn", { "allowConstantExport": true }]
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/Candidate.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | import { useLongPress } from "react-use";
4 |
5 | import CandidateInfo from "./CandidateInfo";
6 | import { LANGUAGE_CODES, ShowRomanization, definitionLayout } from "./consts";
7 |
8 | import type { InterfacePreferences } from "./types";
9 | import type { MouseEvent } from "react";
10 |
11 | export default function Candidate({ isHighlighted, info, selectCandidate, deleteCandidate, showDictionary, hideDictionary, prefs }: {
12 | isHighlighted: boolean;
13 | info: CandidateInfo;
14 | selectCandidate(): void;
15 | deleteCandidate(): void;
16 | showDictionary(): void;
17 | hideDictionary(): void;
18 | prefs: InterfacePreferences;
19 | }) {
20 | const justDeletedCandidate = useRef(false);
21 | function _deleteCandidate() {
22 | deleteCandidate();
23 | justDeletedCandidate.current = true;
24 | }
25 | const { onMouseDown: startLongPress, onMouseUp: cancelLongPress } = useLongPress(_deleteCandidate, { isPreventDefault: false, delay: 800 });
26 | const numOfMoves = useRef(0);
27 | useEffect(() => {
28 | numOfMoves.current = 0;
29 | }, [info]);
30 | function _selectCandidate() {
31 | if (justDeletedCandidate.current) {
32 | justDeletedCandidate.current = false;
33 | }
34 | else {
35 | cancelLongPress();
36 | selectCandidate();
37 | }
38 | }
39 | function _showDictionary(event: MouseEvent) {
40 | if (numOfMoves.current++ >= 2) {
41 | event.preventDefault();
42 | showDictionary();
43 | }
44 | }
45 | function _hideDictionary() {
46 | cancelLongPress();
47 | hideDictionary();
48 | }
49 | const showJyutping = prefs.showRomanization === ShowRomanization.Always || prefs.showRomanization === ShowRomanization.ReverseOnly && info.isReverseLookup;
50 | const commentStyle200 = isHighlighted ? "bg-primary text-primary-content-200" : "text-base-content-200";
51 | const commentStyle300 = isHighlighted ? "bg-primary text-primary-content-300" : "text-base-content-300";
52 | const commentStyle400 = isHighlighted ? "bg-primary text-primary-content-400" : "text-base-content-400";
53 | const labelColSpan = definitionLayout.filter(languages => languages.some(language => prefs.displayLanguages.has(language))).length;
54 | return
55 | {
63 | startLongPress(event);
64 | showDictionary();
65 | }}
66 | onTouchMove={showDictionary}
67 | onTouchEnd={_hideDictionary}
68 | onTouchCancel={_hideDictionary}>
69 | {info.matchedEntries?.map((entry, index) =>
70 |
71 | {!index && info.label}
72 |
73 | {showJyutping && {entry.jyutping}
}
74 | {entry.honzi}
75 |
76 | {!index && (!info.isReverseLookup || prefs.showReverseCode) && info.note}
77 | {entry.isDictionaryEntry(prefs)
78 | ? definitionLayout.flatMap((languages, index) => {
79 | const definitions = languages.flatMap(language =>
80 | prefs.displayLanguages.has(language)
81 | ? [{entry.properties.definition[language]}
]
82 | : []
83 | );
84 | return definitions.length ? [{definitions} ] : [];
85 | })
86 | : !!labelColSpan && {entry.formattedLabels?.join(" ")} }
87 | {!index && info.hasDictionaryEntry(prefs) && "ⓘ"}
88 |
89 | ) ||
90 | {info.label}
91 | {info.text}
92 | {(!info.isReverseLookup || prefs.showReverseCode) && info.note}
93 | }
94 |
95 | ;
96 | }
97 |
--------------------------------------------------------------------------------
/scripts/build_lib.ts:
--------------------------------------------------------------------------------
1 | import { $ } from "bun";
2 | import { argv, cwd } from "process";
3 |
4 | import { patch } from "./utils";
5 |
6 | const root = cwd();
7 | const ENABLE_LOGGING = import.meta.env["ENABLE_LOGGING"] ?? "ON";
8 | const BUILD_TYPE = import.meta.env["BUILD_TYPE"] ?? "Release";
9 | const CXXFLAGS = "-fexceptions -DBOOST_DISABLE_CURRENT_LOCATION";
10 | const DESTDIR = `${root}/build/sysroot`;
11 | const CMAKE_FIND_ROOT_PATH = `${DESTDIR}/usr`;
12 | const CMAKE_DEF = {
13 | raw: `\
14 | -G Ninja \
15 | -DCMAKE_INSTALL_PREFIX:PATH=/usr \
16 | -DCMAKE_BUILD_TYPE:STRING=${BUILD_TYPE} \
17 | -DBUILD_SHARED_LIBS:BOOL=OFF \
18 | `,
19 | };
20 |
21 | $.env({ ...import.meta.env, CXXFLAGS, DESTDIR });
22 |
23 | const targetHandlers = {
24 | async "yaml-cpp"() {
25 | console.log("Building yaml-cpp");
26 | const src = "librime/deps/yaml-cpp";
27 | const dst = "build/yaml-cpp";
28 | await $`rm -rf ${dst}`;
29 | await $`emcmake cmake ${src} -B ${dst} \
30 | ${CMAKE_DEF} \
31 | -DYAML_CPP_BUILD_CONTRIB:BOOL=OFF \
32 | -DYAML_CPP_BUILD_TESTS:BOOL=OFF \
33 | -DYAML_CPP_BUILD_TOOLS:BOOL=OFF \
34 | `;
35 | await $`cmake --build ${dst}`;
36 | await $`cmake --install ${dst}`;
37 | },
38 |
39 | async "leveldb"() {
40 | console.log("Building leveldb");
41 | const src = "librime/deps/leveldb";
42 | const dst = "build/leveldb";
43 | await patch("leveldb.patch", src);
44 | await $`rm -rf ${dst}`;
45 | await $`emcmake cmake ${src} -B ${dst} \
46 | ${CMAKE_DEF} \
47 | -DLEVELDB_BUILD_BENCHMARKS:BOOL=OFF \
48 | -DLEVELDB_BUILD_TESTS:BOOL=OFF \
49 | `;
50 | await $`cmake --build ${dst}`;
51 | await $`cmake --install ${dst}`;
52 | },
53 |
54 | async "marisa"() {
55 | console.log("Building marisa-trie");
56 | const src = "librime/deps/marisa-trie";
57 | const dst = "build/marisa-trie";
58 | await patch("marisa.patch", src);
59 | await $`rm -rf ${dst}`;
60 | await $`emcmake cmake ${src} -B ${dst} ${CMAKE_DEF}`;
61 | await $`cmake --build ${dst}`;
62 | await $`cmake --install ${dst}`;
63 | },
64 |
65 | async "opencc"() {
66 | console.log("Building opencc");
67 | const src = "librime/deps/opencc";
68 | const dst = "build/opencc";
69 | await patch("opencc.patch", src);
70 | await $`rm -rf ${dst}`;
71 | await $`emcmake cmake ${src} -B ${dst} \
72 | ${CMAKE_DEF} \
73 | -DCMAKE_FIND_ROOT_PATH:PATH=${CMAKE_FIND_ROOT_PATH} \
74 | -DSHARE_INSTALL_PREFIX:PATH=/usr/share/rime-data/ \
75 | -DENABLE_DARTS:BOOL=OFF \
76 | -DUSE_SYSTEM_MARISA:BOOL=ON \
77 | `;
78 | await $`cmake --build ${dst}`;
79 | await $`cmake --install ${dst}`;
80 | },
81 |
82 | async "glog"() {
83 | if (ENABLE_LOGGING !== "ON") {
84 | console.log("Skip glog");
85 | return;
86 | }
87 | console.log("Building glog");
88 | const src = "librime/deps/glog";
89 | const dst = "build/glog";
90 | await patch("glog.patch", src);
91 | await $`rm -rf ${dst}`;
92 | await $`emcmake cmake ${src} -B ${dst} \
93 | ${CMAKE_DEF} \
94 | -DWITH_GFLAGS:BOOL=OFF \
95 | -DBUILD_TESTING:BOOL=OFF \
96 | -DWITH_UNWIND:BOOL=OFF \
97 | `;
98 | await $`cmake --build ${dst}`;
99 | await $`cmake --install ${dst}`;
100 | },
101 |
102 | async "rime"() {
103 | console.log("Building librime");
104 | const src = "librime";
105 | const dst = "build/librime_wasm";
106 | await patch("librime.patch", src);
107 | await $`rm -rf ${dst}`;
108 | await $`emcmake cmake ${src} -B ${dst} \
109 | ${CMAKE_DEF} \
110 | -DCMAKE_FIND_ROOT_PATH:PATH=${CMAKE_FIND_ROOT_PATH} \
111 | -DBUILD_TEST:BOOL=OFF \
112 | -DBUILD_STATIC:BOOL=ON \
113 | -DENABLE_THREADING:BOOL=OFF \
114 | -DENABLE_TIMESTAMP:BOOL=OFF \
115 | -DENABLE_LOGGING:BOOL=${ENABLE_LOGGING} \
116 | `;
117 | await $`cmake --build ${dst}`;
118 | await $`cmake --install ${dst}`;
119 | },
120 | };
121 |
122 | const buildTargets = new Set(argv.slice(2));
123 | const unknownTargets = buildTargets.difference(new Set(Object.keys(targetHandlers)));
124 | if (unknownTargets.size) {
125 | throw new Error(`Unknown targets: '${Array.from(unknownTargets).join("', '")}'`);
126 | }
127 |
128 | for (const [target, handler] of Object.entries(targetHandlers)) {
129 | if (!buildTargets.size || buildTargets.has(target)) {
130 | await handler();
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import daisyui from "daisyui";
2 |
3 | import type { Config as DaisyUIConfig } from "daisyui";
4 | import type { Config } from "tailwindcss";
5 |
6 | export default {
7 | important: true,
8 | content: ["./src/**/*"],
9 | theme: {
10 | fontFamily: {
11 | "serif": ["var(--font-serif)", "var(--font-emoji)"],
12 | "sans": ["var(--font-sans)", "var(--font-emoji)"],
13 | "geometric": ["var(--font-geometric)", "var(--font-sans)", "var(--font-emoji)"],
14 | "sung": ["var(--font-sung)", "var(--font-serif)", "var(--font-emoji)"],
15 | "hei": ["var(--font-hei)", "var(--font-sans)", "var(--font-emoji)"],
16 | "kai-fallback-sung": ["var(--font-kai)", "var(--font-sung)", "var(--font-serif)", "var(--font-emoji)"],
17 | "kai-fallback-hei": ["var(--font-kai)", "var(--font-hei)", "var(--font-sans)", "var(--font-emoji)"],
18 | "devanagari": ["var(--font-devanagari)", "var(--font-sans)", "var(--font-emoji)"],
19 | "arabic": ["var(--font-arabic)", "var(--font-sans)", "var(--font-emoji)"],
20 | },
21 | colors: {
22 | "primary-content-200": "rgb(var(--primary-content-200) / )",
23 | "primary-content-300": "rgb(var(--primary-content-300) / )",
24 | "primary-content-400": "rgb(var(--primary-content-400) / )",
25 | "primary-content-500": "rgb(var(--primary-content-500) / )",
26 | "base-400": "rgb(var(--base-400) / )",
27 | "base-500": "rgb(var(--base-500) / )",
28 | "base-content-200": "rgb(var(--base-content-200) / )",
29 | "base-content-300": "rgb(var(--base-content-300) / )",
30 | "base-content-400": "rgb(var(--base-content-400) / )",
31 | },
32 | },
33 | plugins: [daisyui],
34 | daisyui: {
35 | themes: [
36 | {
37 | light: {
38 | "color-scheme": "light",
39 | "primary": "#0a82fa", // highlighted candidate background
40 | "primary-content": "#f8fbff", // highlighted candidate
41 | "--primary-content-200": "229 240 255", // #e5f0ff highlighted label
42 | "--primary-content-300": "212 230 255", // #d4e6ff highlighted pronunciation
43 | "--primary-content-400": "202 225 255", // #cae1ff highlighted definition
44 | "--primary-content-500": "135 195 255", // #87c3ff part of speech border
45 | "secondary": "#d4ebff", // footer background
46 | "secondary-content": "#0659a7", // footer
47 | "accent": "#c7e3ff", // input buffer background
48 | "accent-content": "#05417d", // input buffer
49 | "neutral": "#e5ebf1", // tooltip background
50 | "neutral-content": "#214361", // tooltip
51 | "base-100": "#ffffff", // body background
52 | "base-200": "#f9fafb", // candidate panel background
53 | "base-300": "#eceef1", // toggle buttons background
54 | "--base-400": "222 225 227", // #dee1e3 candidate panel border
55 | "--base-500": "181 183 185", // #b5b7b9 disabled page nav buttons
56 | "base-content": "#001635", // body
57 | "--base-content-200": "75 88 105", // #4b5869 label
58 | "--base-content-300": "67 89 117", // #435975 pronunciation
59 | "--base-content-400": "47 82 120", // #2f5278 definition
60 | },
61 | },
62 | {
63 | dark: {
64 | "color-scheme": "dark",
65 | "primary": "#0465c6", // highlighted candidate background
66 | "primary-content": "#f8fbff", // highlighted candidate
67 | "--primary-content-200": "229 240 255", // #e5f0ff highlighted label
68 | "--primary-content-300": "212 230 255", // #d4e6ff highlighted pronunciation
69 | "--primary-content-400": "202 225 255", // #cae1ff highlighted definition
70 | "--primary-content-500": "69 141 213", // #458dd5 part of speech border
71 | "secondary": "#103f6a", // footer background
72 | "secondary-content": "#d3e0ec", // footer
73 | "accent": "#104b8a", // input buffer background
74 | "accent-content": "#ddecff", // input buffer
75 | "neutral": "#26323e", // tooltip background
76 | "neutral-content": "#c5cfd3", // tooltip
77 | "base-100": "#0b121f", // body background
78 | "base-200": "#1c232a", // candidate panel background
79 | "base-300": "#343a44", // toggle buttons background
80 | "--base-400": "70 77 87", // #464d57 candidate panel border
81 | "--base-500": "116 112 129", // #747a81 disabled page nav buttons
82 | "base-content": "#ffffff", // body
83 | "--base-content-200": "214 224 235", // #d6e0eb label
84 | "--base-content-300": "207 219 232", // #cfdbe8 pronunciation
85 | "--base-content-400": "197 212 228", // #c5d4e4 definition
86 | },
87 | },
88 | ],
89 | } satisfies DaisyUIConfig,
90 | } satisfies Config;
91 |
--------------------------------------------------------------------------------
/src/CandidateInfo.ts:
--------------------------------------------------------------------------------
1 | import { LANGUAGE_CODES, LANGUAGE_NAMES, Language, checkColumns, labels, litColReadings, otherData, partsOfSpeech, registers } from "./consts";
2 | import { ConsumedString, nonEmptyArrayOrUndefined, parseCSV } from "./utils";
3 |
4 | import type { InterfacePreferences } from "./types";
5 |
6 | type KeyNameValue = [key: string, name: string, value: string];
7 |
8 | export default class CandidateInfo {
9 | isReverseLookup: boolean;
10 | note: string;
11 | entries: CandidateEntry[];
12 |
13 | constructor(public label: string, public text: string, commentString = "") {
14 | const comment = new ConsumedString(commentString);
15 | this.isReverseLookup = comment.consume("\v");
16 | this.note = comment.consumeUntil("\f");
17 | this.entries = comment.isNotEmpty
18 | ? comment.consume("\r")
19 | ? String(comment).split("\r").map(csv => new CandidateEntry(csv))
20 | : String(comment).split("\f").map(pron => new CandidateEntry({ honzi: text, jyutping: pron.replace(/\v|; $/g, "") }))
21 | : [];
22 | }
23 |
24 | get matchedEntries() {
25 | return nonEmptyArrayOrUndefined(this.entries.filter(entry => entry.matchInputBuffer === "1"));
26 | }
27 |
28 | hasDictionaryEntry(preferences: InterfacePreferences) {
29 | return this.entries.some(entry => entry.isDictionaryEntry(preferences));
30 | }
31 | }
32 |
33 | export class CandidateEntry {
34 | matchInputBuffer?: string;
35 | honzi?: string;
36 | jyutping?: string;
37 | pronOrder?: string;
38 | sandhi?: string;
39 | litColReading?: string;
40 | properties: {
41 | partOfSpeech?: string;
42 | register?: string;
43 | label?: string;
44 | normalized?: string;
45 | written?: string;
46 | vernacular?: string;
47 | collocation?: string;
48 | definition: Partial>;
49 | };
50 |
51 | isJyutpingOnly: boolean;
52 |
53 | constructor(value: string | { honzi: string; jyutping: string }) {
54 | if ((this.isJyutpingOnly = typeof value === "object")) {
55 | Object.assign(this, value);
56 | this.properties = { definition: {} };
57 | return;
58 | }
59 | // dprint-ignore
60 | const [
61 | matchInputBuffer, honzi, jyutping, pronOrder, sandhi, litColReading,
62 | partOfSpeech, register, label, normalized, written, vernacular, collocation,
63 | eng, urd, nep, hin, ind
64 | ] = parseCSV(value);
65 | this.matchInputBuffer = matchInputBuffer;
66 | this.honzi = honzi;
67 | this.jyutping = jyutping?.replace(/\d(?!$)/g, "$& ");
68 | this.pronOrder = pronOrder;
69 | this.sandhi = sandhi;
70 | this.litColReading = litColReading;
71 | // dprint-ignore
72 | this.properties = {
73 | partOfSpeech, register, label, normalized, written, vernacular, collocation,
74 | definition: { eng, urd, nep, hin, ind }
75 | };
76 | }
77 |
78 | get pronunciationType() {
79 | const types: string[] = [];
80 | if (this.sandhi === "1") types.push("changed tone 變音");
81 | if (this.litColReading! in litColReadings) types.push(litColReadings[this.litColReading!]!);
82 | return types.length ? `(${types.join(", ")})` : undefined;
83 | }
84 |
85 | get formattedPartsOfSpeech() {
86 | return nonEmptyArrayOrUndefined([
87 | ...new Set(
88 | this.properties.partOfSpeech?.split(" ").map(
89 | partOfSpeech => partsOfSpeech[partOfSpeech] || partOfSpeech,
90 | ),
91 | ),
92 | ]);
93 | }
94 |
95 | get formattedRegister() {
96 | return registers[this.properties.register!];
97 | }
98 |
99 | get formattedLabels() {
100 | return nonEmptyArrayOrUndefined([
101 | ...new Set(
102 | this.properties.label?.split(" ").flatMap(word => {
103 | for (const part of word.split("_")) if (labels[part]) return [`(${labels[part]})`];
104 | return [];
105 | }),
106 | ),
107 | ]);
108 | }
109 |
110 | get otherData() {
111 | return nonEmptyArrayOrUndefined(
112 | Object.entries(otherData).flatMap(([name, key]) =>
113 | this.properties[key]
114 | ? [[key, name, this.properties[key]!]]
115 | : []
116 | ),
117 | );
118 | }
119 |
120 | otherLanguages(preferences: InterfacePreferences) {
121 | return nonEmptyArrayOrUndefined(
122 | [...preferences.displayLanguages].flatMap(language =>
123 | language !== preferences.mainLanguage && this.properties.definition[language]
124 | ? [[LANGUAGE_CODES[language], LANGUAGE_NAMES[language], this.properties.definition[language]!]]
125 | : []
126 | ),
127 | );
128 | }
129 |
130 | isDictionaryEntry(preferences: InterfacePreferences) {
131 | return !this.isJyutpingOnly && (checkColumns.some(key => this.properties[key])
132 | || [...preferences.displayLanguages].some(language => this.properties.definition[language]));
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/consts.ts:
--------------------------------------------------------------------------------
1 | import type { CandidateEntry } from "./CandidateInfo";
2 | import type { Preferences } from "./types";
3 |
4 | export const enum Language {
5 | Eng = "eng",
6 | Hin = "hin",
7 | Ind = "ind",
8 | Nep = "nep",
9 | Urd = "urd",
10 | }
11 |
12 | export const enum ShowRomanization {
13 | Always = "always",
14 | ReverseOnly = "reverse_only",
15 | Never = "never",
16 | }
17 |
18 | export const LANGUAGE_CODES: Record = {
19 | [Language.Eng]: "en",
20 | [Language.Hin]: "hi",
21 | [Language.Ind]: "id",
22 | [Language.Nep]: "ne",
23 | [Language.Urd]: "ur",
24 | };
25 |
26 | export const LANGUAGE_NAMES: Record = {
27 | [Language.Eng]: "English",
28 | [Language.Hin]: "Hindi",
29 | [Language.Ind]: "Indonesian",
30 | [Language.Nep]: "Nepali",
31 | [Language.Urd]: "Urdu",
32 | };
33 |
34 | export const LANGUAGE_LABELS: Record = {
35 | [Language.Eng]: "英語 English",
36 | [Language.Hin]: "印地語 Hindi",
37 | [Language.Ind]: "印尼語 Indonesian",
38 | [Language.Nep]: "尼泊爾語 Nepali",
39 | [Language.Urd]: "烏爾都語 Urdu",
40 | };
41 |
42 | export const SHOW_ROMANIZATION_LABELS: Record = {
43 | [ShowRomanization.Always]: "顯示 Always Show",
44 | [ShowRomanization.ReverseOnly]: "僅反查 Only in Reverse Lookup",
45 | [ShowRomanization.Never]: "隱藏 Hide",
46 | };
47 |
48 | export const DEFAULT_PREFERENCES: Preferences = {
49 | displayLanguages: new Set([Language.Eng]),
50 | mainLanguage: Language.Eng,
51 | pageSize: 6,
52 | isHeiTypeface: false,
53 | showRomanization: ShowRomanization.Always,
54 | enableCompletion: true,
55 | enableCorrection: false,
56 | enableSentence: true,
57 | enableLearning: true,
58 | showReverseCode: true,
59 | isCangjie5: true,
60 | };
61 |
62 | export const NO_AUTO_FILL = {
63 | autoComplete: "off",
64 | autoCorrect: "off",
65 | autoCapitalize: "off",
66 | spellCheck: "false",
67 | } as const;
68 |
69 | export const definitionLayout = [[Language.Eng, Language.Ind], [Language.Hin, Language.Nep], [Language.Urd]];
70 |
71 | export const otherData: Record> = {
72 | "Standard Form 標準字形": "normalized",
73 | "Written Form 書面語": "written",
74 | "Vernacular Form 口語": "vernacular",
75 | "Collocation 配搭": "collocation",
76 | };
77 |
78 | export const litColReadings: Record = {
79 | lit: "literary reading 文讀",
80 | col: "colloquial reading 白讀",
81 | };
82 |
83 | export const registers: Record = {
84 | wri: "written 書面語",
85 | ver: "vernacular 口語",
86 | for: "formal 公文體",
87 | lzh: "classical Chinese 文言",
88 | };
89 |
90 | export const partsOfSpeech: Record = {
91 | n: "noun 名詞",
92 | v: "verb 動詞",
93 | adj: "adjective 形容詞",
94 | adv: "adverb 副詞",
95 | morph: "morpheme 語素",
96 | mw: "measure word 量詞",
97 | part: "particle 助詞",
98 | oth: "other 其他",
99 | x: "non-morpheme 非語素",
100 | };
101 |
102 | export const labels: Record = {
103 | abbrev: "abbreviation 簡稱",
104 | astro: "astronomy 天文",
105 | ChinMeta: "sexagenary cycle 干支",
106 | horo: "horoscope 星座",
107 | org: "organisation 機構",
108 | person: "person 人名",
109 | place: "place 地名",
110 | reli: "religion 宗教",
111 | rare: "rare 罕見",
112 | composition: "compound 詞組",
113 | };
114 |
115 | export const checkColumns: (keyof CandidateEntry["properties"])[] = [
116 | "partOfSpeech",
117 | "register",
118 | "normalized",
119 | "written",
120 | "vernacular",
121 | "collocation",
122 | ];
123 |
124 | export const RIME_KEY_MAP: Record = {
125 | "Escape": "Escape",
126 | "F4": "F4",
127 | "Backspace": "BackSpace",
128 | "Delete": "Delete",
129 | "Tab": "Tab",
130 | "Enter": "Return",
131 | "Home": "Home",
132 | "End": "End",
133 | "PageUp": "Page_Up",
134 | "PageDown": "Page_Down",
135 | "ArrowUp": "Up",
136 | "ArrowRight": "Right",
137 | "ArrowDown": "Down",
138 | "ArrowLeft": "Left",
139 | "~": "asciitilde",
140 | "`": "quoteleft",
141 | "!": "exclam",
142 | "@": "at",
143 | "#": "numbersign",
144 | "$": "dollar",
145 | "%": "percent",
146 | "^": "asciicircum",
147 | "&": "ampersand",
148 | "*": "asterisk",
149 | "(": "parenleft",
150 | ")": "parenright",
151 | "-": "minus",
152 | "_": "underscore",
153 | "+": "plus",
154 | "=": "equal",
155 | "{": "braceleft",
156 | "[": "bracketleft",
157 | "}": "braceright",
158 | "]": "bracketright",
159 | ":": "colon",
160 | ";": "semicolon",
161 | '"': "quotedbl",
162 | "'": "apostrophe",
163 | "|": "bar",
164 | "\\": "backslash",
165 | "<": "less",
166 | ",": "comma",
167 | ">": "greater",
168 | ".": "period",
169 | "?": "question",
170 | "/": "slash",
171 | " ": "space",
172 | };
173 |
--------------------------------------------------------------------------------
/src/worker.ts:
--------------------------------------------------------------------------------
1 | import type { Actions, ListenerArgsMap, Message, RimeResult, RimeAPI, RimeNotification, RimeDeployStatus } from "./types";
2 |
3 | type TypeToString = T extends number ? "number"
4 | : T extends string ? "string"
5 | : T extends readonly unknown[] ? "array"
6 | : T extends boolean ? "boolean"
7 | : null;
8 |
9 | type ArgsToString = { [P in keyof T]: TypeToString };
10 |
11 | interface Module {
12 | ccall(
13 | ident: Method,
14 | returnType: TypeToString>,
15 | argTypes: ArgsToString>,
16 | args: Parameters,
17 | opts?: Emscripten.CCallOpts,
18 | ): ReturnType;
19 | FS: typeof FS;
20 | }
21 |
22 | declare const Module: Module;
23 |
24 | interface PredefinedModule {
25 | printErr: (message: string) => void;
26 | onRuntimeInitialized: () => void;
27 | locateFile: (path: string, prefix: string) => string;
28 | }
29 |
30 | declare const globalThis: {
31 | onRimeNotification(type: T, value: RimeNotification[T]): void;
32 | Module: PredefinedModule;
33 | };
34 |
35 | globalThis.onRimeNotification = (type, value) => {
36 | switch (type) {
37 | case "deploy":
38 | dispatch("deployStatusChanged", value as RimeDeployStatus);
39 | break;
40 | case "schema":
41 | dispatch("schemaChanged", ...value.split("/") as [string, string]);
42 | break;
43 | case "option": {
44 | const disabled = value[0] === "!";
45 | dispatch("optionChanged", value.slice(+disabled), !disabled);
46 | break;
47 | }
48 | }
49 | };
50 |
51 | function dispatch(name: K, ...args: ListenerArgsMap[K]) {
52 | postMessage({ type: "listener", name, args });
53 | }
54 |
55 | function syncUserDirectory(direction: "read" | "write") {
56 | return new Promise((resolve, reject) => {
57 | Module.FS.syncfs(direction === "read", (err?: Error) => err ? reject(err) : resolve());
58 | });
59 | }
60 |
61 | const actions: Actions = {
62 | async setOption(option, value) {
63 | Module.ccall("set_option", null, ["string", "number"], [option, +value]);
64 | },
65 | async processKey(input) {
66 | const result = JSON.parse(Module.ccall("process_key", "string", ["string"], [input])) as RimeResult;
67 | if ("committed" in result) {
68 | await syncUserDirectory("write");
69 | }
70 | return result;
71 | },
72 | async selectCandidate(index) {
73 | return JSON.parse(Module.ccall("select_candidate", "string", ["number"], [index])) as RimeResult;
74 | },
75 | async deleteCandidate(index) {
76 | return JSON.parse(Module.ccall("delete_candidate", "string", ["number"], [index])) as RimeResult;
77 | },
78 | async flipPage(backward) {
79 | return JSON.parse(Module.ccall("flip_page", "string", ["boolean"], [backward])) as RimeResult;
80 | },
81 | async customize({ pageSize, enableCompletion, enableCorrection, enableSentence, enableLearning, isCangjie5 }) {
82 | return Module.ccall("customize", "boolean", ["number", "number"], [
83 | pageSize,
84 | [!isCangjie5, !enableLearning, !enableSentence, enableCorrection, !enableCompletion, /* showCangjieRoots */ true]
85 | .reduce((flags, curr) => (flags << 1) | +curr, 0),
86 | ]);
87 | },
88 | async deploy() {
89 | const result = Module.ccall("deploy", "boolean", [], []);
90 | await syncUserDirectory("write");
91 | return result;
92 | },
93 | };
94 |
95 | let loading = true;
96 | const RIME_USER_DIR = "/rime";
97 | const loadRime = new Promise(resolve => {
98 | globalThis.Module = {
99 | printErr(message) {
100 | if (process.env.NODE_ENV !== "production" || location.search === "?debug") {
101 | const match = /^([IWEF])\S+ \S+ \S+ (.*)$/.exec(message);
102 | if (match) {
103 | console[({ I: "info", W: "warn", E: "error", F: "error" } as const)[match[1] as "I" | "W" | "E" | "F"]](`[${match[2]}`);
104 | }
105 | else {
106 | console.error(message);
107 | }
108 | }
109 | },
110 | async onRuntimeInitialized() {
111 | Module.FS.mkdir(RIME_USER_DIR);
112 | Module.FS.mount(IDBFS, {}, RIME_USER_DIR);
113 | await syncUserDirectory("read");
114 | const success = Module.ccall("init", "boolean", [], []);
115 | await syncUserDirectory("write");
116 | loading = false;
117 | dispatch("initialized", success);
118 | resolve();
119 | },
120 | locateFile(path) {
121 | return path;
122 | },
123 | };
124 | importScripts("rime.js");
125 | });
126 |
127 | addEventListener("message", async ({ data: { name, args } }: MessageEvent) => {
128 | if (loading) await loadRime;
129 | try {
130 | // @ts-expect-error Unactionable
131 | const result = await actions[name](...args);
132 | postMessage({ type: "success", result });
133 | }
134 | catch (error) {
135 | postMessage({ type: "error", error });
136 | }
137 | });
138 |
--------------------------------------------------------------------------------
/src/Preferences.tsx:
--------------------------------------------------------------------------------
1 | import { LANGUAGE_LABELS, SHOW_ROMANIZATION_LABELS, Language, ShowRomanization } from "./consts";
2 | import { Radio, RadioCheckbox, Segment, Toggle } from "./Inputs";
3 |
4 | import type { PreferencesWithSetter } from "./types";
5 |
6 | export default function Preferences(prefs: PreferencesWithSetter) {
7 | function toggleDisplayLanguage(language: Language, checked: boolean) {
8 | const newDisplayLanguages = new Set(prefs.displayLanguages);
9 | newDisplayLanguages[checked ? "add" : "delete"](language);
10 | prefs.setDisplayLanguages(newDisplayLanguages);
11 | }
12 | return
13 | 輸入法設定 IME Settings
14 |
93 | 反查設定 Reverse Lookup Settings
94 |
95 |
96 |
97 |
98 |
99 |
100 |
倉頡版本 Cangjie Version
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | ;
109 | }
110 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | html {
7 | font-size: 12px;
8 | @screen sm {
9 | font-size: 16px;
10 | }
11 | }
12 | @font-face {
13 | font-family: "Urdu Typesetting";
14 | src: local("Urdu Typesetting");
15 | size-adjust: 140%;
16 | descent-override: 70%;
17 | }
18 | @font-face {
19 | font-family: "Noto Nastaliq Urdu";
20 | src: local("Noto Nastaliq Urdu");
21 | descent-override: 70%;
22 | }
23 | :root {
24 | --btn-text-case: none;
25 | --toastify-toast-width: auto;
26 | --toastify-font-family: var(--font-sans), var(--font-emoji);
27 | /* prettier-ignore */
28 | --font-sans: "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Ubuntu", "Helvetica Neue", "Noto Sans", "Liberation Sans", "Arial",
29 | "Microsoft JhengHei", "Microsoft JhengHei UI", "Noto Sans HK", "Noto Sans CJK HK", "sans-serif";
30 | /* prettier-ignore */
31 | --font-serif: "Times New Roman", "Times", "Georgia", "Cambria", "Noto Serif",
32 | "MingLiU_HKSCS", "MingLiU_HKSCS-ExtB", "MingLiU", "MingLiU-ExtB", "PMingLiU", "PMingLiU-ExtB", "Noto Serif HK", "Noto Serif CJK HK", "serif";
33 | --font-geometric: "Century Gothic", "Avenir Next", "Avenir", "Futura";
34 | --font-sung: "Chiron Sung HK", "Chiron Sung HK WS";
35 | --font-hei: "Chiron Hei HK", "Chiron Hei HK WS";
36 | --font-kai: "DFHKStdKai-B5", "BiauKaiHK", "BiauKai", "DFKai-SB", "KaiU", "Kaiti TC";
37 | --font-devanagari: "Nirmala UI", "Mangal";
38 | --font-arabic: "Urdu Typesetting", "Noto Nastaliq Urdu", "Tahoma";
39 | --font-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
40 | }
41 | a {
42 | @apply link link-primary no-underline transition-colors;
43 | }
44 | :lang(en) {
45 | @apply font-sans text-[12pt];
46 | }
47 | :lang(id) {
48 | @apply font-serif text-[12pt];
49 | }
50 | :lang(hi),
51 | :lang(ne) {
52 | @apply font-devanagari text-[11pt];
53 | }
54 | :lang(ur) {
55 | @apply font-arabic text-[10pt];
56 | }
57 | }
58 | .tooltip {
59 | @apply before:whitespace-pre-line;
60 | --tooltip-tail: 0.375rem;
61 | --tooltip-offset: calc(100% + var(--tooltip-tail));
62 | }
63 | .btn-toolbar {
64 | @apply btn btn-square text-xl bg-base-300 border-base-300 hover:bg-base-400 hover:border-base-400;
65 | }
66 | .candidate-panel {
67 | @apply flex absolute shadow-lg border-[0.1875rem] border-base-400 bg-base-200 rounded-[0.5625rem] z-50 overflow-hidden leading-snug whitespace-nowrap;
68 | }
69 | .page-nav {
70 | @apply btn btn-ghost text-primary text-4xl h-[unset] min-h-[unset] leading-[1.875rem] px-2 rounded-md hover:bg-base-400 hover:border-base-400 disabled:bg-transparent disabled:text-base-500;
71 | }
72 | .candidates td {
73 | @apply p-1 align-bottom text-left empty:p-0;
74 | @supports selector(:has(*)) {
75 | &:has(*):not(:has(:not(:empty))) {
76 | @apply p-0;
77 | }
78 | }
79 | }
80 | .candidates :lang(ur) {
81 | @apply text-right;
82 | }
83 | .candidates button {
84 | tr:first-child td:first-child {
85 | @apply rounded-ss-md min-w-1.5;
86 | }
87 | tr:first-child td:last-child {
88 | @apply rounded-se-md min-w-1.5;
89 | }
90 | tr:last-child td:first-child {
91 | @apply rounded-es-md min-w-1.5;
92 | }
93 | tr:last-child td:last-child {
94 | @apply rounded-ee-md min-w-1.5;
95 | }
96 | }
97 | @supports selector(:has(*)) {
98 | .candidate-panel:has(.dictionary-panel) .candidates .highlighted tr {
99 | @apply relative;
100 | &::after {
101 | @apply content-[""] absolute block w-4 -top-2 -right-2;
102 | height: calc(100% + 1rem);
103 | background-size: 1rem 1rem;
104 | background-position:
105 | top left,
106 | bottom left;
107 | background-repeat: no-repeat;
108 | /* prettier-ignore */
109 | background-image: radial-gradient(circle at top left, transparent 0.5rem, theme(colors.primary) calc(0.5rem + 1px)),
110 | radial-gradient(circle at bottom left, transparent 0.5rem, theme(colors.primary) calc(0.5rem + 1px));
111 | }
112 | }
113 | }
114 | .dictionary-panel {
115 | @apply px-5 pt-3 pb-4 rounded-md bg-primary text-primary-content text-[12pt] whitespace-normal min-w-48;
116 | }
117 | .dictionary-panel > *:not(:last-child) {
118 | @apply mb-3.5;
119 | }
120 | .dictionary-entry > *:not(:last-child) {
121 | @apply mb-2.5;
122 | }
123 | .entry-head > *:not(:first-child),
124 | .entry-body > *:not(:first-child) {
125 | @apply ml-4;
126 | }
127 | .pos {
128 | @apply font-light text-[10pt] leading-7 text-primary-content-300 border border-primary-content-500 rounded p-0.5 whitespace-nowrap;
129 | }
130 | .lbl {
131 | @apply text-primary-content-300 whitespace-nowrap;
132 | }
133 | .pos + .pos,
134 | .lbl + .lbl {
135 | @apply ml-1;
136 | }
137 | .dictionary-panel {
138 | th {
139 | @apply text-right text-primary-content-300 align-baseline font-normal whitespace-nowrap;
140 | }
141 | td {
142 | @apply align-baseline pl-3;
143 | }
144 | }
145 | .tick {
146 | @apply relative;
147 | &::after {
148 | @apply absolute top-full left-1/2 -translate-x-1/2 text-sm leading-6;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/scripts/build_native.ts:
--------------------------------------------------------------------------------
1 | import { $ } from "bun";
2 | import { platform } from "os";
3 | import { argv, cwd, exit } from "process";
4 |
5 | import { patch } from "./utils";
6 |
7 | const root = cwd();
8 | const PLATFORM = platform();
9 |
10 | const dst = "build";
11 | const dstRime = "build/librime_native";
12 |
13 | const CMAKE_INSTALL_PREFIX = `"${root}/librime"`;
14 | const CMAKE_DEF_COMMON = `\
15 | -G Ninja \
16 | -DCMAKE_BUILD_TYPE:STRING=Release ${
17 | PLATFORM === "win32"
18 | ? `\
19 | -DCMAKE_C_COMPILER=clang \
20 | -DCMAKE_CXX_COMPILER=clang++ \
21 | -DCMAKE_USER_MAKE_RULES_OVERRIDE:PATH="${root}/librime/cmake/c_flag_overrides.cmake" \
22 | -DCMAKE_USER_MAKE_RULES_OVERRIDE_CXX:PATH="${root}/librime/cmake/cxx_flag_overrides.cmake" \
23 | -DCMAKE_EXE_LINKER_FLAGS_INIT:STRING=-llibcmt \
24 | -DCMAKE_MSVC_RUNTIME_LIBRARY:STRING=MultiThreaded`
25 | : ""
26 | }`;
27 | const CMAKE_DEF = {
28 | raw: `\
29 | -B ${dst} \
30 | ${CMAKE_DEF_COMMON} \
31 | -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_INSTALL_PREFIX} \
32 | -DBUILD_SHARED_LIBS:BOOL=OFF \
33 | `,
34 | };
35 | const CMAKE_DEF_RIME = {
36 | raw: `\
37 | -B ${dstRime} \
38 | ${CMAKE_DEF_COMMON} \
39 | -DBUILD_SHARED_LIBS:BOOL=ON \
40 | ${PLATFORM === "linux" ? "" : "-DBUILD_STATIC:BOOL=ON"} \
41 | -DBUILD_TEST:BOOL=OFF \
42 | -DBoost_INCLUDE_DIR:PATH="${root}/build/sysroot/usr/include" \
43 | -DENABLE_TIMESTAMP:BOOL=OFF \
44 | -DENABLE_LOGGING:BOOL=OFF \
45 | `,
46 | };
47 |
48 | if (PLATFORM !== "linux") {
49 | $.env({ ...import.meta.env, BOOST_ROOT: `${root}/boost` });
50 | }
51 |
52 | let hasError = false;
53 |
54 | const targetHandlers = {
55 | async "yaml-cpp"() {
56 | console.log("Building yaml-cpp");
57 | $.cwd("librime/deps/yaml-cpp");
58 | await $`rm -rf ${dst}`;
59 | await $`cmake . \
60 | ${CMAKE_DEF} \
61 | -DYAML_CPP_BUILD_CONTRIB:BOOL=OFF \
62 | -DYAML_CPP_BUILD_TESTS:BOOL=OFF \
63 | -DYAML_CPP_BUILD_TOOLS:BOOL=OFF \
64 | `;
65 | await $`cmake --build ${dst}`;
66 | await $`cmake --install ${dst}`;
67 | },
68 |
69 | async "leveldb"() {
70 | console.log("Building leveldb");
71 | $.cwd("librime/deps/leveldb");
72 | await $`rm -rf ${dst}`;
73 | await $`cmake . \
74 | ${CMAKE_DEF} \
75 | -DCMAKE_CXX_FLAGS:STRING=-Wno-error=deprecated-declarations \
76 | -DLEVELDB_BUILD_BENCHMARKS:BOOL=OFF \
77 | -DLEVELDB_BUILD_TESTS:BOOL=OFF \
78 | `;
79 | await $`cmake --build ${dst}`;
80 | await $`cmake --install ${dst}`;
81 | },
82 |
83 | async "marisa"() {
84 | console.log("Building marisa-trie");
85 | $.cwd("librime/deps/marisa-trie");
86 | await patch("marisa.patch");
87 | await $`rm -rf ${dst}`;
88 | await $`cmake . ${CMAKE_DEF}`;
89 | await $`cmake --build ${dst}`;
90 | await $`cmake --install ${dst}`;
91 | },
92 |
93 | async "opencc"() {
94 | console.log("Building opencc");
95 | $.cwd("librime/deps/opencc");
96 | await patch("opencc.patch");
97 | await $`rm -rf ${dst}`;
98 | await $`cmake . \
99 | ${CMAKE_DEF} \
100 | -DCMAKE_FIND_ROOT_PATH:PATH=${CMAKE_INSTALL_PREFIX} \
101 | -DENABLE_DARTS:BOOL=OFF \
102 | -DUSE_SYSTEM_MARISA:BOOL=ON \
103 | `;
104 | await $`cmake --build ${dst}`;
105 | await $`cmake --install ${dst}`;
106 | },
107 |
108 | async "glog"() {
109 | console.error(`'glog' need not be built in phase 'native'`);
110 | hasError = true;
111 | },
112 |
113 | async "rime"() {
114 | console.log("Building librime");
115 | $.cwd();
116 | await patch("librime.patch", "librime");
117 | await $`rm -rf ${dstRime}`;
118 | await $`cmake librime ${CMAKE_DEF_RIME}`;
119 | await $`cmake --build ${dstRime}`;
120 | },
121 | };
122 |
123 | function needNotBeBuilt(target: string) {
124 | return async () => {
125 | console.error(`'${target}' need not be built in Linux in phase 'native'`);
126 | hasError = true;
127 | };
128 | }
129 |
130 | const nonPosixTargets: (keyof typeof targetHandlers)[] = ["yaml-cpp", "leveldb", "marisa", "opencc"];
131 | if (PLATFORM === "linux") {
132 | for (const target of nonPosixTargets) {
133 | targetHandlers[target] = needNotBeBuilt(target);
134 | }
135 | }
136 |
137 | const buildTargets = new Set(argv.slice(2));
138 | const unknownTargets = buildTargets.difference(new Set(Object.keys(targetHandlers)));
139 | if (unknownTargets.size) {
140 | throw new Error(`Unknown targets: '${Array.from(unknownTargets).join("', '")}'`);
141 | }
142 |
143 | for (const [target, handler] of Object.entries(targetHandlers)) {
144 | if (buildTargets.has(target)) {
145 | await handler();
146 | }
147 | }
148 | if (hasError) {
149 | exit(1);
150 | }
151 |
152 | if (!buildTargets.size) {
153 | const allTargets: (keyof typeof targetHandlers)[] = [...(PLATFORM === "linux" ? [] : nonPosixTargets), "rime"];
154 | for (const handler of allTargets.map(target => targetHandlers[target])) {
155 | await handler();
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/wasm/api.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #define APP_NAME "TypeDuck-Web"
10 | #define PAGE_SIZE_KEY "menu/page_size"
11 | #define PATCH_DIRECTIVE "__patch"
12 |
13 | const char* SETTINGS_PATCH_ITEMS[] = {
14 | "common:/show_cangjie_roots", "common:/disable_completion",
15 | "common:/enable_correction", "common:/disable_sentence",
16 | "common:/disable_learning", "common:/use_cangjie3"};
17 |
18 | namespace typeduck {
19 |
20 | RimeTraits traits = {0};
21 | RimeSessionId session_id;
22 | RimeCommit commit;
23 | RimeContext context;
24 | std::string json_string;
25 | RimeApi* rime = rime_get_api();
26 |
27 | bool settings_initialized = false;
28 | RimeLeversApi* levers_api = NULL;
29 | RimeCustomSettings* default_settings = NULL;
30 | RimeCustomSettings* common_settings = NULL;
31 |
32 | void handler(void*, RimeSessionId, const char* type, const char* value) {
33 | EM_ASM(onRimeNotification(UTF8ToString($0), UTF8ToString($1)), type, value);
34 | }
35 |
36 | bool start_rime(bool restart) {
37 | rime->initialize(&traits);
38 | rime->set_notification_handler(handler, NULL);
39 | if (restart ? rime->start_maintenance(true) : rime->start_quick()) {
40 | rime->join_maintenance_thread();
41 | return true;
42 | }
43 | return false;
44 | }
45 |
46 | bool stop_rime() {
47 | if (rime->destroy_session(session_id)) {
48 | rime->finalize();
49 | return true;
50 | }
51 | return false;
52 | }
53 |
54 | const char* process(Bool success) {
55 | boost::json::object result;
56 | result["success"] = !!success;
57 | rime->free_commit(&commit);
58 | if (rime->get_commit(session_id, &commit)) {
59 | result["committed"] = commit.text;
60 | }
61 | rime->free_context(&context);
62 | rime->get_context(session_id, &context);
63 | result["isComposing"] = !!context.composition.length;
64 | if (context.composition.length) {
65 | RimeComposition& composition = context.composition;
66 | std::string preedit = composition.preedit;
67 | boost::json::object pre_edit;
68 | pre_edit["before"] = preedit.substr(0, composition.sel_start);
69 | pre_edit["active"] = preedit.substr(
70 | composition.sel_start, composition.sel_end - composition.sel_start);
71 | pre_edit["after"] = preedit.substr(composition.sel_end);
72 | result["inputBuffer"] = pre_edit;
73 | RimeMenu& menu = context.menu;
74 | result["page"] = menu.page_no;
75 | result["isLastPage"] = !!menu.is_last_page;
76 | result["highlightedIndex"] = menu.highlighted_candidate_index;
77 | boost::json::array candidates;
78 | for (size_t i = 0; i < menu.num_candidates; ++i) {
79 | boost::json::object candidate;
80 | if (context.select_labels) {
81 | result["label"] = context.select_labels[i];
82 | }
83 | candidate["text"] = menu.candidates[i].text;
84 | if (menu.candidates[i].comment) {
85 | candidate["comment"] = menu.candidates[i].comment;
86 | }
87 | candidates.push_back(candidate);
88 | }
89 | result["candidates"] = candidates;
90 | }
91 | json_string = boost::json::serialize(result);
92 | return json_string.c_str();
93 | }
94 |
95 | extern "C" {
96 |
97 | bool init() {
98 | RIME_STRUCT_INIT(RimeTraits, traits);
99 | traits.shared_data_dir = "/usr/share/rime-data";
100 | traits.user_data_dir = "/rime";
101 | traits.app_name = APP_NAME;
102 | rime->setup(&traits);
103 | RIME_STRUCT_INIT(RimeCommit, commit);
104 | RIME_STRUCT_INIT(RimeContext, context);
105 | if (start_rime(false)) {
106 | session_id = rime->create_session();
107 | return true;
108 | }
109 | return false;
110 | }
111 |
112 | void set_option(const char* option, int value) {
113 | rime->set_option(session_id, option, value);
114 | }
115 |
116 | const char* process_key(const char* input) {
117 | return process(rime->simulate_key_sequence(session_id, input));
118 | }
119 |
120 | const char* select_candidate(int index) {
121 | return process(rime->select_candidate_on_current_page(session_id, index));
122 | }
123 |
124 | const char* delete_candidate(int index) {
125 | return process(rime->delete_candidate_on_current_page(session_id, index));
126 | }
127 |
128 | const char* flip_page(bool backward) {
129 | return process(rime->change_page(session_id, backward));
130 | }
131 |
132 | bool customize(int page_size, int options) {
133 | if (!settings_initialized) {
134 | levers_api = (RimeLeversApi*)rime->find_module("levers")->get_api();
135 | default_settings = levers_api->custom_settings_init("default", APP_NAME);
136 | common_settings = levers_api->custom_settings_init("common", APP_NAME);
137 | settings_initialized = true;
138 | }
139 | Bool success = true;
140 | success &=
141 | levers_api->customize_int(default_settings, PAGE_SIZE_KEY, page_size);
142 | success &= levers_api->save_settings(default_settings);
143 | RimeConfig config = {0};
144 | success &= rime->config_init(&config);
145 | success &= rime->config_create_list(&config, "");
146 | for (size_t i = 0; i < sizeof(SETTINGS_PATCH_ITEMS); ++i) {
147 | if (options & (1 << i)) {
148 | success &=
149 | rime->config_list_append_string(&config, "", SETTINGS_PATCH_ITEMS[i]);
150 | }
151 | }
152 | success &= rime->config_set_item(&config, "", &config);
153 | success &=
154 | levers_api->customize_item(common_settings, PATCH_DIRECTIVE, &config);
155 | success &= levers_api->save_settings(common_settings);
156 | return !!success;
157 | }
158 |
159 | bool deploy() {
160 | if (stop_rime() && start_rime(true)) {
161 | session_id = rime->create_session();
162 | return true;
163 | }
164 | return false;
165 | }
166 | }
167 |
168 | } // namespace typeduck
169 |
--------------------------------------------------------------------------------
/src/CandidatePanel.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useCallback } from "react";
2 |
3 | import Candidate from "./Candidate";
4 | import CandidateInfo from "./CandidateInfo";
5 | import CaretFollower from "./CaretFollower";
6 | import { RIME_KEY_MAP } from "./consts";
7 | import DictionaryPanel from "./DictionaryPanel";
8 | import Rime from "./rime";
9 | import { isPrintable, notify } from "./utils";
10 |
11 | import type { InputState, InterfacePreferences, RimeResult } from "./types";
12 |
13 | export default function CandidatePanel({ runAsyncTask, textArea, prefs, deployStatus }: {
14 | runAsyncTask(asyncTask: () => Promise): void;
15 | textArea: HTMLTextAreaElement;
16 | prefs: InterfacePreferences;
17 | deployStatus: number;
18 | }) {
19 | const [inputState, setInputState] = useState();
20 | const [showDictionaryIndex, setShowDictionaryIndex] = useState();
21 | const candidateList = useRef(null);
22 | const dictionaryPanel = useRef(null);
23 |
24 | const hideDictionary = useCallback(() => {
25 | setShowDictionaryIndex(undefined);
26 | }, [setShowDictionaryIndex]);
27 |
28 | const insert = useCallback((newText: string) => {
29 | const { selectionStart, selectionEnd } = textArea;
30 | textArea.value = textArea.value.slice(0, selectionStart) + newText + textArea.value.slice(selectionEnd);
31 | textArea.selectionStart = textArea.selectionEnd = selectionStart + newText.length;
32 | }, [textArea]);
33 |
34 | const handleRimeResult = useCallback((promise: Promise, key?: string) =>
35 | runAsyncTask(async () => {
36 | let type: "warning" | "error" | undefined;
37 | try {
38 | const result = await promise;
39 | if (!result.success) {
40 | type = "warning";
41 | }
42 | const state = result.isComposing
43 | ? {
44 | inputBuffer: result.inputBuffer,
45 | highlightedIndex: result.highlightedIndex,
46 | candidates: result.candidates.map(
47 | ({ label, text, comment }, i) => new CandidateInfo(label || `${(i + 1) % 10}.`, text, comment),
48 | ),
49 | isPrevDisabled: !result.page,
50 | isNextDisabled: result.isLastPage,
51 | }
52 | : inputState;
53 | if (result.committed) {
54 | insert(result.committed);
55 | }
56 | else if (!state && key && isPrintable(key)) {
57 | insert(key);
58 | }
59 | setInputState(result.isComposing ? state : undefined);
60 | hideDictionary();
61 | }
62 | catch {
63 | type = "error";
64 | }
65 | if (type) {
66 | notify(type, "執行操作", "performing the operation");
67 | }
68 | textArea.focus();
69 | }), [hideDictionary, inputState, insert, runAsyncTask, textArea]);
70 |
71 | const processKey = useCallback((input: string, key?: string) => handleRimeResult(Rime.processKey(input), key), [handleRimeResult]);
72 | const flipPage = useCallback((backward: boolean) => handleRimeResult(Rime.flipPage(backward)), [handleRimeResult]);
73 | const selectCandidate = useCallback((index: number) => handleRimeResult(Rime.selectCandidate(index)), [handleRimeResult]);
74 | const deleteCandidate = useCallback((index: number) => handleRimeResult(Rime.deleteCandidate(index)), [handleRimeResult]);
75 |
76 | const parseKey = useCallback((event: KeyboardEvent) => {
77 | const { code, key } = event;
78 | const hasControl = event.getModifierState("Control");
79 | const hasMeta = event.getModifierState("Meta");
80 | const hasAlt = event.getModifierState("Alt");
81 | const hasShift = event.getModifierState("Shift");
82 | if (
83 | (inputState || (
84 | document.activeElement === textArea
85 | && (!hasControl && (isPrintable(key) || !hasShift && key === "F4") || key === "`")
86 | && !hasMeta
87 | && !hasAlt
88 | )) && code
89 | ) {
90 | const match = /^(Control|Meta|Alt|Shift)(Left|Right)$/.exec(code);
91 | const isNumpadKey = code.startsWith("Numpad");
92 | const modifiers = new Set();
93 | if (hasControl) {
94 | modifiers.add("Control");
95 | }
96 | if (hasMeta) {
97 | modifiers.add("Meta");
98 | }
99 | if (hasAlt) {
100 | modifiers.add("Alt");
101 | }
102 | if (hasShift) {
103 | modifiers.add("Shift");
104 | }
105 | if (match) {
106 | modifiers.delete(match[1]);
107 | modifiers.add(`${match[1]}_${match[2][0]}`);
108 | }
109 | else {
110 | let rimeKey = isNumpadKey ? code.slice(6) : key;
111 | rimeKey = RIME_KEY_MAP[rimeKey] || rimeKey;
112 | modifiers.add(isNumpadKey ? `KP_${rimeKey}` : rimeKey);
113 | }
114 | return [...modifiers].join("+");
115 | }
116 | return undefined;
117 | }, [inputState, textArea]);
118 |
119 | useEffect(() => {
120 | function onKeyDown(event: KeyboardEvent) {
121 | const key = parseKey(event);
122 | if (key) {
123 | event.preventDefault();
124 | processKey(`{${key}}`, event.key);
125 | }
126 | }
127 | function onKeyUp(event: KeyboardEvent) {
128 | if (inputState) {
129 | const key = parseKey(event);
130 | if (key) processKey(`{Release+${key}}`);
131 | }
132 | }
133 | document.addEventListener("keydown", onKeyDown);
134 | document.addEventListener("keyup", onKeyUp);
135 | return () => {
136 | document.removeEventListener("keydown", onKeyDown);
137 | document.removeEventListener("keyup", onKeyUp);
138 | };
139 | }, [inputState, parseKey, processKey]);
140 |
141 | useEffect(() => {
142 | setInputState(undefined);
143 | hideDictionary();
144 | }, [deployStatus, setInputState, hideDictionary]);
145 |
146 | const hideDictionaryOnLeaveCandidate = useCallback(() => {
147 | function hideDictionaryOnLeaveDictionaryPanel() {
148 | if (!candidateList.current?.matches(":hover")) {
149 | hideDictionary();
150 | }
151 | dictionaryPanel.current?.removeEventListener("mouseleave", hideDictionaryOnLeaveDictionaryPanel);
152 | dictionaryPanel.current?.removeEventListener("touchend", hideDictionaryOnLeaveDictionaryPanel);
153 | }
154 | if (dictionaryPanel.current?.matches(":hover")) {
155 | dictionaryPanel.current.addEventListener("mouseleave", hideDictionaryOnLeaveDictionaryPanel);
156 | dictionaryPanel.current.addEventListener("touchend", hideDictionaryOnLeaveDictionaryPanel);
157 | }
158 | else if (!candidateList.current?.matches(":hover")) {
159 | hideDictionary();
160 | }
161 | }, [hideDictionary]);
162 |
163 | const shouldShowDictionary = typeof showDictionaryIndex === "number";
164 | return inputState &&
165 |
166 |
167 |
168 | {inputState.inputBuffer.before && {inputState.inputBuffer.before} }
169 | {inputState.inputBuffer.active && {inputState.inputBuffer.active} }
170 | {inputState.inputBuffer.after && {inputState.inputBuffer.after} }
171 |
172 |
173 | flipPage(true)}>
174 | ‹
175 |
176 | flipPage(false)}>
177 | ›
178 |
179 |
180 |
181 |
182 | {inputState.candidates.map((candidate, index) =>
183 | selectCandidate(index)}
188 | deleteCandidate={() => deleteCandidate(index)}
189 | showDictionary={() => setShowDictionaryIndex(index)}
190 | hideDictionary={hideDictionaryOnLeaveCandidate}
191 | prefs={prefs} />
192 | )}
193 |
194 |
195 | {shouldShowDictionary && }
196 | ;
197 | }
198 |
--------------------------------------------------------------------------------
/patches/glog.patch:
--------------------------------------------------------------------------------
1 | diff --git a/CMakeLists.txt b/CMakeLists.txt
2 | index b787631..d8bda34 100644
3 | --- a/CMakeLists.txt
4 | +++ b/CMakeLists.txt
5 | @@ -43,6 +43,8 @@ option (WITH_TLS "Enable Thread Local Storage (TLS) support" ON)
6 | set (WITH_UNWIND libunwind CACHE STRING "unwind driver")
7 | set_property (CACHE WITH_UNWIND PROPERTY STRINGS none unwind libunwind)
8 |
9 | +add_definitions(-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.)
10 | +
11 | cmake_dependent_option (WITH_GMOCK "Use Google Mock" ON WITH_GTEST OFF)
12 |
13 | set (WITH_FUZZING none CACHE STRING "Fuzzing engine")
14 | diff --git a/src/glog/logging.h b/src/glog/logging.h
15 | index 9ab897e..a7b2564 100644
16 | --- a/src/glog/logging.h
17 | +++ b/src/glog/logging.h
18 | @@ -358,9 +358,9 @@ struct [[deprecated("Use LogMessage instead.")]] LogMessageInfo {
19 | // better to have compact code for these operations.
20 |
21 | #if GOOGLE_STRIP_LOG == 0
22 | -# define COMPACT_GOOGLE_LOG_INFO google::LogMessage(__FILE__, __LINE__)
23 | +# define COMPACT_GOOGLE_LOG_INFO google::LogMessage(__FILE_NAME__, __LINE__)
24 | # define LOG_TO_STRING_INFO(message) \
25 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_INFO, message)
26 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_INFO, message)
27 | #else
28 | # define COMPACT_GOOGLE_LOG_INFO google::NullStream()
29 | # define LOG_TO_STRING_INFO(message) google::NullStream()
30 | @@ -368,9 +368,9 @@ struct [[deprecated("Use LogMessage instead.")]] LogMessageInfo {
31 |
32 | #if GOOGLE_STRIP_LOG <= 1
33 | # define COMPACT_GOOGLE_LOG_WARNING \
34 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_WARNING)
35 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_WARNING)
36 | # define LOG_TO_STRING_WARNING(message) \
37 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_WARNING, message)
38 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_WARNING, message)
39 | #else
40 | # define COMPACT_GOOGLE_LOG_WARNING google::NullStream()
41 | # define LOG_TO_STRING_WARNING(message) google::NullStream()
42 | @@ -378,18 +378,18 @@ struct [[deprecated("Use LogMessage instead.")]] LogMessageInfo {
43 |
44 | #if GOOGLE_STRIP_LOG <= 2
45 | # define COMPACT_GOOGLE_LOG_ERROR \
46 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_ERROR)
47 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_ERROR)
48 | # define LOG_TO_STRING_ERROR(message) \
49 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_ERROR, message)
50 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_ERROR, message)
51 | #else
52 | # define COMPACT_GOOGLE_LOG_ERROR google::NullStream()
53 | # define LOG_TO_STRING_ERROR(message) google::NullStream()
54 | #endif
55 |
56 | #if GOOGLE_STRIP_LOG <= 3
57 | -# define COMPACT_GOOGLE_LOG_FATAL google::LogMessageFatal(__FILE__, __LINE__)
58 | +# define COMPACT_GOOGLE_LOG_FATAL google::LogMessageFatal(__FILE_NAME__, __LINE__)
59 | # define LOG_TO_STRING_FATAL(message) \
60 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_FATAL, message)
61 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_FATAL, message)
62 | #else
63 | # define COMPACT_GOOGLE_LOG_FATAL google::NullStreamFatal()
64 | # define LOG_TO_STRING_FATAL(message) google::NullStreamFatal()
65 | @@ -407,40 +407,40 @@ struct [[deprecated("Use LogMessage instead.")]] LogMessageInfo {
66 | # define COMPACT_GOOGLE_LOG_DFATAL COMPACT_GOOGLE_LOG_ERROR
67 | #elif GOOGLE_STRIP_LOG <= 3
68 | # define COMPACT_GOOGLE_LOG_DFATAL \
69 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_FATAL)
70 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_FATAL)
71 | #else
72 | # define COMPACT_GOOGLE_LOG_DFATAL google::NullStreamFatal()
73 | #endif
74 |
75 | #define GOOGLE_LOG_INFO(counter) \
76 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_INFO, counter, \
77 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_INFO, counter, \
78 | &google::LogMessage::SendToLog)
79 | #define SYSLOG_INFO(counter) \
80 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_INFO, counter, \
81 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_INFO, counter, \
82 | &google::LogMessage::SendToSyslogAndLog)
83 | #define GOOGLE_LOG_WARNING(counter) \
84 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_WARNING, counter, \
85 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_WARNING, counter, \
86 | &google::LogMessage::SendToLog)
87 | #define SYSLOG_WARNING(counter) \
88 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_WARNING, counter, \
89 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_WARNING, counter, \
90 | &google::LogMessage::SendToSyslogAndLog)
91 | #define GOOGLE_LOG_ERROR(counter) \
92 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_ERROR, counter, \
93 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_ERROR, counter, \
94 | &google::LogMessage::SendToLog)
95 | #define SYSLOG_ERROR(counter) \
96 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_ERROR, counter, \
97 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_ERROR, counter, \
98 | &google::LogMessage::SendToSyslogAndLog)
99 | #define GOOGLE_LOG_FATAL(counter) \
100 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_FATAL, counter, \
101 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_FATAL, counter, \
102 | &google::LogMessage::SendToLog)
103 | #define SYSLOG_FATAL(counter) \
104 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_FATAL, counter, \
105 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_FATAL, counter, \
106 | &google::LogMessage::SendToSyslogAndLog)
107 | #define GOOGLE_LOG_DFATAL(counter) \
108 | - google::LogMessage(__FILE__, __LINE__, google::DFATAL_LEVEL, counter, \
109 | + google::LogMessage(__FILE_NAME__, __LINE__, google::DFATAL_LEVEL, counter, \
110 | &google::LogMessage::SendToLog)
111 | #define SYSLOG_DFATAL(counter) \
112 | - google::LogMessage(__FILE__, __LINE__, google::DFATAL_LEVEL, counter, \
113 | + google::LogMessage(__FILE_NAME__, __LINE__, google::DFATAL_LEVEL, counter, \
114 | &google::LogMessage::SendToSyslogAndLog)
115 |
116 | #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || \
117 | @@ -457,7 +457,7 @@ struct [[deprecated("Use LogMessage instead.")]] LogMessageInfo {
118 | std::unique_ptr release{message, \
119 | &LocalFree}; \
120 | if (message_length > 0) { \
121 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_ERROR, 0, \
122 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_ERROR, 0, \
123 | &google::LogMessage::SendToLog) \
124 | .stream() \
125 | << reinterpret_cast(message); \
126 | @@ -546,11 +546,11 @@ class LogSink; // defined below
127 | // LogSeverity severity;
128 | // The cast is to disambiguate nullptr arguments.
129 | #define LOG_TO_SINK(sink, severity) \
130 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_##severity, \
131 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_##severity, \
132 | static_cast(sink), true) \
133 | .stream()
134 | #define LOG_TO_SINK_BUT_NOT_TO_LOGFILE(sink, severity) \
135 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_##severity, \
136 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_##severity, \
137 | static_cast(sink), false) \
138 | .stream()
139 |
140 | @@ -766,7 +766,7 @@ using _Check_string = std::string;
141 | google::logging::internal::GetReferenceableValue(val1), \
142 | google::logging::internal::GetReferenceableValue(val2), \
143 | #val1 " " #op " " #val2)) \
144 | - log(__FILE__, __LINE__, \
145 | + log(__FILE_NAME__, __LINE__, \
146 | google::logging::internal::CheckOpString(std::move(_result))) \
147 | .stream()
148 | #else
149 | @@ -778,7 +778,7 @@ using _Check_string = std::string;
150 | google::logging::internal::GetReferenceableValue(val1), \
151 | google::logging::internal::GetReferenceableValue(val2), \
152 | #val1 " " #op " " #val2)) \
153 | - log(__FILE__, __LINE__, _result).stream()
154 | + log(__FILE_NAME__, __LINE__, _result).stream()
155 | #endif // STATIC_ANALYSIS, DCHECK_IS_ON()
156 |
157 | #if GOOGLE_STRIP_LOG <= 3
158 | @@ -819,7 +819,7 @@ using _Check_string = std::string;
159 |
160 | #define CHECK_NOTNULL(val) \
161 | google::logging::internal::CheckNotNull( \
162 | - __FILE__, __LINE__, "'" #val "' Must be non nullptr", (val))
163 | + __FILE_NAME__, __LINE__, "'" #val "' Must be non nullptr", (val))
164 |
165 | // Helper functions for string comparisons.
166 | // To avoid bloat, the definitions are in logging.cc.
167 | @@ -881,7 +881,7 @@ DECLARE_CHECK_STROP_IMPL(strcasecmp, false)
168 | #define PLOG(severity) GOOGLE_PLOG(severity, 0).stream()
169 |
170 | #define GOOGLE_PLOG(severity, counter) \
171 | - google::ErrnoLogMessage(__FILE__, __LINE__, google::GLOG_##severity, \
172 | + google::ErrnoLogMessage(__FILE_NAME__, __LINE__, google::GLOG_##severity, \
173 | counter, &google::LogMessage::SendToLog)
174 |
175 | #define PLOG_IF(severity, condition) \
176 | @@ -933,9 +933,9 @@ namespace google {
177 | std::chrono::duration(seconds)); \
178 | static std::atomic LOG_PREVIOUS_TIME_RAW; \
179 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
180 | - __FILE__, __LINE__, &LOG_TIME_PERIOD, sizeof(google::int64), "")); \
181 | + __FILE_NAME__, __LINE__, &LOG_TIME_PERIOD, sizeof(google::int64), "")); \
182 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
183 | - __FILE__, __LINE__, &LOG_PREVIOUS_TIME_RAW, sizeof(google::int64), "")); \
184 | + __FILE_NAME__, __LINE__, &LOG_PREVIOUS_TIME_RAW, sizeof(google::int64), "")); \
185 | const auto LOG_CURRENT_TIME = \
186 | std::chrono::duration_cast( \
187 | std::chrono::steady_clock::now().time_since_epoch()); \
188 | @@ -949,54 +949,54 @@ namespace google {
189 | .count(), \
190 | std::memory_order_relaxed); \
191 | if (LOG_TIME_DELTA > LOG_TIME_PERIOD) \
192 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_##severity).stream()
193 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_##severity).stream()
194 |
195 | #define SOME_KIND_OF_LOG_EVERY_N(severity, n, what_to_do) \
196 | static std::atomic LOG_OCCURRENCES(0), LOG_OCCURRENCES_MOD_N(0); \
197 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
198 | - __FILE__, __LINE__, &LOG_OCCURRENCES, sizeof(int), "")); \
199 | + __FILE_NAME__, __LINE__, &LOG_OCCURRENCES, sizeof(int), "")); \
200 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
201 | - __FILE__, __LINE__, &LOG_OCCURRENCES_MOD_N, sizeof(int), "")); \
202 | + __FILE_NAME__, __LINE__, &LOG_OCCURRENCES_MOD_N, sizeof(int), "")); \
203 | ++LOG_OCCURRENCES; \
204 | if (++LOG_OCCURRENCES_MOD_N > n) LOG_OCCURRENCES_MOD_N -= n; \
205 | if (LOG_OCCURRENCES_MOD_N == 1) \
206 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_##severity, \
207 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_##severity, \
208 | LOG_OCCURRENCES, &what_to_do) \
209 | .stream()
210 |
211 | #define SOME_KIND_OF_LOG_IF_EVERY_N(severity, condition, n, what_to_do) \
212 | static std::atomic LOG_OCCURRENCES(0), LOG_OCCURRENCES_MOD_N(0); \
213 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
214 | - __FILE__, __LINE__, &LOG_OCCURRENCES, sizeof(int), "")); \
215 | + __FILE_NAME__, __LINE__, &LOG_OCCURRENCES, sizeof(int), "")); \
216 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
217 | - __FILE__, __LINE__, &LOG_OCCURRENCES_MOD_N, sizeof(int), "")); \
218 | + __FILE_NAME__, __LINE__, &LOG_OCCURRENCES_MOD_N, sizeof(int), "")); \
219 | ++LOG_OCCURRENCES; \
220 | if ((condition) && \
221 | ((LOG_OCCURRENCES_MOD_N = (LOG_OCCURRENCES_MOD_N + 1) % n) == (1 % n))) \
222 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_##severity, \
223 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_##severity, \
224 | LOG_OCCURRENCES, &what_to_do) \
225 | .stream()
226 |
227 | #define SOME_KIND_OF_PLOG_EVERY_N(severity, n, what_to_do) \
228 | static std::atomic LOG_OCCURRENCES(0), LOG_OCCURRENCES_MOD_N(0); \
229 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
230 | - __FILE__, __LINE__, &LOG_OCCURRENCES, sizeof(int), "")); \
231 | + __FILE_NAME__, __LINE__, &LOG_OCCURRENCES, sizeof(int), "")); \
232 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
233 | - __FILE__, __LINE__, &LOG_OCCURRENCES_MOD_N, sizeof(int), "")); \
234 | + __FILE_NAME__, __LINE__, &LOG_OCCURRENCES_MOD_N, sizeof(int), "")); \
235 | ++LOG_OCCURRENCES; \
236 | if (++LOG_OCCURRENCES_MOD_N > n) LOG_OCCURRENCES_MOD_N -= n; \
237 | if (LOG_OCCURRENCES_MOD_N == 1) \
238 | - google::ErrnoLogMessage(__FILE__, __LINE__, google::GLOG_##severity, \
239 | + google::ErrnoLogMessage(__FILE_NAME__, __LINE__, google::GLOG_##severity, \
240 | LOG_OCCURRENCES, &what_to_do) \
241 | .stream()
242 |
243 | #define SOME_KIND_OF_LOG_FIRST_N(severity, n, what_to_do) \
244 | static std::atomic LOG_OCCURRENCES(0); \
245 | GLOG_IFDEF_THREAD_SANITIZER(AnnotateBenignRaceSized( \
246 | - __FILE__, __LINE__, &LOG_OCCURRENCES, sizeof(int), "")); \
247 | + __FILE_NAME__, __LINE__, &LOG_OCCURRENCES, sizeof(int), "")); \
248 | if (LOG_OCCURRENCES <= n) ++LOG_OCCURRENCES; \
249 | if (LOG_OCCURRENCES <= n) \
250 | - google::LogMessage(__FILE__, __LINE__, google::GLOG_##severity, \
251 | + google::LogMessage(__FILE_NAME__, __LINE__, google::GLOG_##severity, \
252 | LOG_OCCURRENCES, &what_to_do) \
253 | .stream()
254 |
255 | @@ -1418,7 +1418,7 @@ class GLOG_EXPORT LogMessageFatal : public LogMessage {
256 | // A non-macro interface to the log facility; (useful
257 | // when the logging level is not a compile-time constant).
258 | inline void LogAtLevel(LogSeverity severity, std::string const& msg) {
259 | - LogMessage(__FILE__, __LINE__, severity).stream() << msg;
260 | + LogMessage(__FILE_NAME__, __LINE__, severity).stream() << msg;
261 | }
262 |
263 | // A macro alternative of LogAtLevel. New code may want to use this
264 | @@ -1426,7 +1426,7 @@ inline void LogAtLevel(LogSeverity severity, std::string const& msg) {
265 | // file name and the line number where this macro is put like other
266 | // LOG macros, 2. this macro can be used as C++ stream.
267 | #define LOG_AT_LEVEL(severity) \
268 | - google::LogMessage(__FILE__, __LINE__, severity).stream()
269 | + google::LogMessage(__FILE_NAME__, __LINE__, severity).stream()
270 |
271 | // Allow folks to put a counter in the LOG_EVERY_X()'ed messages. This
272 | // only works if ostream is a LogStream. If the ostream is not a
273 |
--------------------------------------------------------------------------------