diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e6b7895 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +# end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b76d347 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +libs/** +**/libs/** +**/*.min.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..44fb88a --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,68 @@ +module.exports = { + root: true, + extends: [ + 'eslint:recommended', + 'plugin:jsdoc/recommended', + ], + plugins: [ + 'jsdoc', + 'security', + 'no-unsanitized', + ], + env: { + browser: true, + jquery: true, + es6: true, + }, + globals: { + toastr: 'readonly', + Fuse: 'readonly', + globalThis: 'readonly', + SillyTavern: 'readonly', + ePub: 'readonly', + pdfjsLib: 'readonly', + echarts: 'readonly', + }, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + 'no-eval': 'warn', + 'no-implied-eval': 'warn', + 'no-new-func': 'warn', + 'no-script-url': 'warn', + 'no-unsanitized/method': 'warn', + 'no-unsanitized/property': 'warn', + 'security/detect-object-injection': 'off', + 'security/detect-non-literal-regexp': 'off', + 'security/detect-unsafe-regex': 'off', + 'no-restricted-syntax': [ + 'warn', + { + selector: 'CallExpression[callee.property.name="postMessage"][arguments.1.value="*"]', + message: 'Avoid postMessage(..., "*"); use a trusted origin or the shared iframe messaging helper.', + }, + { + selector: 'CallExpression[callee.property.name="addEventListener"][arguments.0.value="message"]', + message: 'All message listeners must validate origin/source (use isTrustedMessage).', + }, + ], + 'no-undef': 'error', + 'no-unused-vars': ['warn', { args: 'none' }], + 'eqeqeq': 'off', + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-inner-declarations': 'off', + 'no-constant-condition': ['error', { checkLoops: false }], + 'no-useless-catch': 'off', + 'no-control-regex': 'off', + 'no-mixed-spaces-and-tabs': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/check-types': 'off', + 'jsdoc/tag-lines': 'off', + }, +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md index a3e0008..b575156 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,7 @@ -# LittleWhiteBox +# LittleWhiteBox -## 📁 目录结构 +一个面向 SillyTavern 的多功能扩展,包含剧情总结/记忆系统、变量系统、任务与多种面板能力。集成了画图、流式生成、模板编辑、调试面板等组件,适合用于复杂玩法与长期剧情记录。 -``` -LittleWhiteBox/ -├── index.js # 入口:初始化/注册所有模块 -├── manifest.json # 插件清单:版本/依赖/入口 -├── settings.html # 主设置页:模块开关/UI -├── style.css # 全局样式 -├── README.md # 说明文档 -├── .eslintrc.cjs # ESLint 规则 -├── .eslintignore # ESLint 忽略 -├── .gitignore # Git 忽略 -├── package.json # 开发依赖/脚本 -├── package-lock.json # 依赖锁定 -├── jsconfig.json # 编辑器提示 -│ -├── core/ # 核心基础设施(不直接做功能UI) -│ ├── constants.js # 常量/路径 -│ ├── event-manager.js # 统一事件管理 -│ ├── debug-core.js # 日志/缓存注册 -│ ├── slash-command.js # 斜杠命令封装 -│ ├── variable-path.js # 变量路径解析 -│ ├── server-storage.js # 服务器存储(防抖/重试) -│ ├── wrapper-inline.js # iframe 内联脚本 -│ └── iframe-messaging.js # postMessage 封装与 origin 校验 -│ -├── widgets/ # 通用UI组件(跨功能复用) -│ ├── message-toolbar.js # 消息区工具条注册/管理 -│ └── button-collapse.js # 消息区按钮收纳 -│ -├── modules/ # 功能模块(每个功能自带UI) -│ ├── control-audio.js # 音频权限控制 -│ ├── iframe-renderer.js # iframe 渲染 -│ ├── immersive-mode.js # 沉浸模式 -│ ├── message-preview.js # 消息预览/拦截 -│ ├── streaming-generation.js # 生成相关功能(xbgenraw) -│ │ -│ ├── debug-panel/ # 调试面板 -│ │ ├── debug-panel.js # 悬浮窗控制 -│ │ └── debug-panel.html # UI -│ │ -│ ├── fourth-wall/ # 四次元壁 -│ │ ├── fourth-wall.js # 逻辑 -│ │ ├── fourth-wall.html # UI -│ │ ├── fw-image.js # 图像交互 -│ │ ├── fw-message-enhancer.js # 消息增强 -│ │ ├── fw-prompt.js # 提示词编辑 -│ │ └── fw-voice.js # 语音展示 -│ │ -│ ├── novel-draw/ # 画图 -│ │ ├── novel-draw.js # 主逻辑 -│ │ ├── novel-draw.html # UI -│ │ ├── llm-service.js # LLM 分析 -│ │ ├── floating-panel.js # 悬浮面板 -│ │ ├── gallery-cache.js # 缓存 -│ │ ├── image-live-effect.js # Live 动效 -│ │ ├── cloud-presets.js # 云预设 -│ │ └── TAG编写指南.md # 文档 -│ │ -│ ├── tts/ # TTS -│ │ ├── tts.js # 主逻辑 -│ │ ├── tts-auth-provider.js # 鉴权 -│ │ ├── tts-free-provider.js # 试用 -│ │ ├── tts-api.js # API -│ │ ├── tts-text.js # 文本处理 -│ │ ├── tts-player.js # 播放器 -│ │ ├── tts-panel.js # 气泡UI -│ │ ├── tts-cache.js # 缓存 -│ │ ├── tts-overlay.html # 设置UI -│ │ ├── tts-voices.js # 音色数据 -│ │ ├── 开通管理.png # 说明图 -│ │ ├── 获取ID和KEY.png # 说明图 -│ │ └── 声音复刻.png # 说明图 -│ │ -│ ├── scheduled-tasks/ # 定时任务 -│ │ ├── scheduled-tasks.js # 调度 -│ │ ├── scheduled-tasks.html # UI -│ │ └── embedded-tasks.html # 嵌入UI -│ │ -│ ├── template-editor/ # 模板编辑器 -│ │ ├── template-editor.js # 逻辑 -│ │ └── template-editor.html # UI -│ │ -│ ├── story-outline/ # 故事大纲 -│ │ ├── story-outline.js # 逻辑 -│ │ ├── story-outline.html # UI -│ │ └── story-outline-prompt.js # 提示词 -│ │ -│ ├── story-summary/ # 剧情总结 -│ │ ├── story-summary.js # 逻辑 -│ │ ├── story-summary.html # UI -│ │ └── llm-service.js # LLM 服务 -│ │ -│ └── variables/ # 变量系统 -│ ├── var-commands.js # 命令 -│ ├── varevent-editor.js # 编辑器 -│ ├── variables-core.js # 核心 -│ └── variables-panel.js # 面板 -│ -├── bridges/ # 外部服务桥接 -│ ├── call-generate-service.js # ST 生成服务 -│ ├── worldbook-bridge.js # 世界书桥接 -│ └── wrapper-iframe.js # iframe 客户端脚本 -│ -├── libs/ # 第三方库 -│ └── pixi.min.js # PixiJS -│ -└── docs/ # 许可/声明 - ├── COPYRIGHT - ├── LICENSE.md - └── NOTICE +## 许可证 -node_modules/ # 本地依赖(不提交) -``` - -## 📄 许可证 - -详见 `docs/LICENSE.md` \ No newline at end of file +详见 `docs/LICENSE.md` diff --git a/core/debug-core.js b/core/debug-core.js index 0abc5d0..f05ea36 100644 --- a/core/debug-core.js +++ b/core/debug-core.js @@ -1,4 +1,4 @@ -import { EventCenter } from "./event-manager.js"; +import { EventCenter } from "./event-manager.js"; const DEFAULT_MAX_LOGS = 200; @@ -110,14 +110,14 @@ class LoggerCore { }); } - info(moduleId, message) { - this._log("info", moduleId, message, null); + info(moduleId, ...args) { + const msg = args.map(a => (typeof a === 'string' ? a : safeStringify(a))).join(' '); + this._log('info', moduleId, msg, null); } - - warn(moduleId, message) { - this._log("warn", moduleId, message, null); + warn(moduleId, ...args) { + const msg = args.map(a => (typeof a === 'string' ? a : safeStringify(a))).join(' '); + this._log('warn', moduleId, msg, null); } - error(moduleId, message, err) { this._log("error", moduleId, message, err || null); } diff --git a/core/server-storage.js b/core/server-storage.js index 2459d41..4797524 100644 --- a/core/server-storage.js +++ b/core/server-storage.js @@ -183,3 +183,4 @@ export const StoryOutlineStorage = new StorageFile('LittleWhiteBox_StoryOutline. export const NovelDrawStorage = new StorageFile('LittleWhiteBox_NovelDraw.json', { debounceMs: 800 }); export const TtsStorage = new StorageFile('LittleWhiteBox_TTS.json', { debounceMs: 800 }); export const CommonSettingStorage = new StorageFile('LittleWhiteBox_CommonSettings.json', { debounceMs: 1000 }); +export const VectorStorage = new StorageFile('LittleWhiteBox_Vectors.json', { debounceMs: 3000 }); diff --git a/docs/NOTICE b/docs/NOTICE index 1d189ae..f10911b 100644 --- a/docs/NOTICE +++ b/docs/NOTICE @@ -93,3 +93,7 @@ For complete license terms, see LICENSE.md For attribution requirements, see COPYRIGHT Last updated: 2025-01-14 +TinySegmenter 0.2 +Copyright (c) 2008 Taku Kudo +MIT License +http://www.chasen.org/~taku/software/TinySegmenter/ diff --git a/index.js b/index.js index 61c90b6..6496f13 100644 --- a/index.js +++ b/index.js @@ -40,6 +40,7 @@ extension_settings[EXT_ID] = extension_settings[EXT_ID] || { audio: { enabled: true }, variablesPanel: { enabled: false }, variablesCore: { enabled: true }, + variablesMode: '1.0', storySummary: { enabled: true }, storyOutline: { enabled: false }, novelDraw: { enabled: false }, @@ -273,7 +274,7 @@ function toggleSettingsControls(enabled) { 'scheduled_tasks_enabled', 'xiaobaix_template_enabled', 'xiaobaix_immersive_enabled', 'xiaobaix_fourth_wall_enabled', 'xiaobaix_audio_enabled', 'xiaobaix_variables_panel_enabled', - 'xiaobaix_use_blob', 'xiaobaix_variables_core_enabled', 'Wrapperiframe', 'xiaobaix_render_enabled', + 'xiaobaix_use_blob', 'xiaobaix_variables_core_enabled', 'xiaobaix_variables_mode', 'Wrapperiframe', 'xiaobaix_render_enabled', 'xiaobaix_max_rendered', 'xiaobaix_story_outline_enabled', 'xiaobaix_story_summary_enabled', 'xiaobaix_novel_draw_enabled', 'xiaobaix_novel_draw_open_settings', 'xiaobaix_tts_enabled', 'xiaobaix_tts_open_settings' @@ -430,6 +431,15 @@ async function setupSettings() { }); }); + // variables mode selector + $("#xiaobaix_variables_mode") + .val(settings.variablesMode || "1.0") + .on("change", function () { + settings.variablesMode = String($(this).val() || "1.0"); + saveSettingsDebounced(); + toastr.info(`变量系统已切换为 ${settings.variablesMode}`); + }); + $("#xiaobaix_novel_draw_open_settings").on("click", function () { if (!isXiaobaixEnabled) return; if (settings.novelDraw?.enabled && window.xiaobaixNovelDraw?.openSettings) { diff --git a/libs/dexie.mjs b/libs/dexie.mjs new file mode 100644 index 0000000..fe55a2a --- /dev/null +++ b/libs/dexie.mjs @@ -0,0 +1,5912 @@ +/* + * Dexie.js - a minimalistic wrapper for IndexedDB + * =============================================== + * + * By David Fahlander, david.fahlander@gmail.com + * + * Version 4.0.10, Fri Nov 15 2024 + * + * https://dexie.org + * + * Apache License Version 2.0, January 2004, http://www.apache.org/licenses/ + */ + +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); +}; +function __extends(d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +} +var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +function __spreadArray(to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +} + +var _global = typeof globalThis !== 'undefined' ? globalThis : + typeof self !== 'undefined' ? self : + typeof window !== 'undefined' ? window : + global; + +var keys = Object.keys; +var isArray = Array.isArray; +if (typeof Promise !== 'undefined' && !_global.Promise) { + _global.Promise = Promise; +} +function extend(obj, extension) { + if (typeof extension !== 'object') + return obj; + keys(extension).forEach(function (key) { + obj[key] = extension[key]; + }); + return obj; +} +var getProto = Object.getPrototypeOf; +var _hasOwn = {}.hasOwnProperty; +function hasOwn(obj, prop) { + return _hasOwn.call(obj, prop); +} +function props(proto, extension) { + if (typeof extension === 'function') + extension = extension(getProto(proto)); + (typeof Reflect === "undefined" ? keys : Reflect.ownKeys)(extension).forEach(function (key) { + setProp(proto, key, extension[key]); + }); +} +var defineProperty = Object.defineProperty; +function setProp(obj, prop, functionOrGetSet, options) { + defineProperty(obj, prop, extend(functionOrGetSet && hasOwn(functionOrGetSet, "get") && typeof functionOrGetSet.get === 'function' ? + { get: functionOrGetSet.get, set: functionOrGetSet.set, configurable: true } : + { value: functionOrGetSet, configurable: true, writable: true }, options)); +} +function derive(Child) { + return { + from: function (Parent) { + Child.prototype = Object.create(Parent.prototype); + setProp(Child.prototype, "constructor", Child); + return { + extend: props.bind(null, Child.prototype) + }; + } + }; +} +var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +function getPropertyDescriptor(obj, prop) { + var pd = getOwnPropertyDescriptor(obj, prop); + var proto; + return pd || (proto = getProto(obj)) && getPropertyDescriptor(proto, prop); +} +var _slice = [].slice; +function slice(args, start, end) { + return _slice.call(args, start, end); +} +function override(origFunc, overridedFactory) { + return overridedFactory(origFunc); +} +function assert(b) { + if (!b) + throw new Error("Assertion Failed"); +} +function asap$1(fn) { + if (_global.setImmediate) + setImmediate(fn); + else + setTimeout(fn, 0); +} +function arrayToObject(array, extractor) { + return array.reduce(function (result, item, i) { + var nameAndValue = extractor(item, i); + if (nameAndValue) + result[nameAndValue[0]] = nameAndValue[1]; + return result; + }, {}); +} +function getByKeyPath(obj, keyPath) { + if (typeof keyPath === 'string' && hasOwn(obj, keyPath)) + return obj[keyPath]; + if (!keyPath) + return obj; + if (typeof keyPath !== 'string') { + var rv = []; + for (var i = 0, l = keyPath.length; i < l; ++i) { + var val = getByKeyPath(obj, keyPath[i]); + rv.push(val); + } + return rv; + } + var period = keyPath.indexOf('.'); + if (period !== -1) { + var innerObj = obj[keyPath.substr(0, period)]; + return innerObj == null ? undefined : getByKeyPath(innerObj, keyPath.substr(period + 1)); + } + return undefined; +} +function setByKeyPath(obj, keyPath, value) { + if (!obj || keyPath === undefined) + return; + if ('isFrozen' in Object && Object.isFrozen(obj)) + return; + if (typeof keyPath !== 'string' && 'length' in keyPath) { + assert(typeof value !== 'string' && 'length' in value); + for (var i = 0, l = keyPath.length; i < l; ++i) { + setByKeyPath(obj, keyPath[i], value[i]); + } + } + else { + var period = keyPath.indexOf('.'); + if (period !== -1) { + var currentKeyPath = keyPath.substr(0, period); + var remainingKeyPath = keyPath.substr(period + 1); + if (remainingKeyPath === "") + if (value === undefined) { + if (isArray(obj) && !isNaN(parseInt(currentKeyPath))) + obj.splice(currentKeyPath, 1); + else + delete obj[currentKeyPath]; + } + else + obj[currentKeyPath] = value; + else { + var innerObj = obj[currentKeyPath]; + if (!innerObj || !hasOwn(obj, currentKeyPath)) + innerObj = (obj[currentKeyPath] = {}); + setByKeyPath(innerObj, remainingKeyPath, value); + } + } + else { + if (value === undefined) { + if (isArray(obj) && !isNaN(parseInt(keyPath))) + obj.splice(keyPath, 1); + else + delete obj[keyPath]; + } + else + obj[keyPath] = value; + } + } +} +function delByKeyPath(obj, keyPath) { + if (typeof keyPath === 'string') + setByKeyPath(obj, keyPath, undefined); + else if ('length' in keyPath) + [].map.call(keyPath, function (kp) { + setByKeyPath(obj, kp, undefined); + }); +} +function shallowClone(obj) { + var rv = {}; + for (var m in obj) { + if (hasOwn(obj, m)) + rv[m] = obj[m]; + } + return rv; +} +var concat = [].concat; +function flatten(a) { + return concat.apply([], a); +} +var intrinsicTypeNames = "BigUint64Array,BigInt64Array,Array,Boolean,String,Date,RegExp,Blob,File,FileList,FileSystemFileHandle,FileSystemDirectoryHandle,ArrayBuffer,DataView,Uint8ClampedArray,ImageBitmap,ImageData,Map,Set,CryptoKey" + .split(',').concat(flatten([8, 16, 32, 64].map(function (num) { return ["Int", "Uint", "Float"].map(function (t) { return t + num + "Array"; }); }))).filter(function (t) { return _global[t]; }); +var intrinsicTypes = new Set(intrinsicTypeNames.map(function (t) { return _global[t]; })); +function cloneSimpleObjectTree(o) { + var rv = {}; + for (var k in o) + if (hasOwn(o, k)) { + var v = o[k]; + rv[k] = !v || typeof v !== 'object' || intrinsicTypes.has(v.constructor) ? v : cloneSimpleObjectTree(v); + } + return rv; +} +function objectIsEmpty(o) { + for (var k in o) + if (hasOwn(o, k)) + return false; + return true; +} +var circularRefs = null; +function deepClone(any) { + circularRefs = new WeakMap(); + var rv = innerDeepClone(any); + circularRefs = null; + return rv; +} +function innerDeepClone(x) { + if (!x || typeof x !== 'object') + return x; + var rv = circularRefs.get(x); + if (rv) + return rv; + if (isArray(x)) { + rv = []; + circularRefs.set(x, rv); + for (var i = 0, l = x.length; i < l; ++i) { + rv.push(innerDeepClone(x[i])); + } + } + else if (intrinsicTypes.has(x.constructor)) { + rv = x; + } + else { + var proto = getProto(x); + rv = proto === Object.prototype ? {} : Object.create(proto); + circularRefs.set(x, rv); + for (var prop in x) { + if (hasOwn(x, prop)) { + rv[prop] = innerDeepClone(x[prop]); + } + } + } + return rv; +} +var toString = {}.toString; +function toStringTag(o) { + return toString.call(o).slice(8, -1); +} +var iteratorSymbol = typeof Symbol !== 'undefined' ? + Symbol.iterator : + '@@iterator'; +var getIteratorOf = typeof iteratorSymbol === "symbol" ? function (x) { + var i; + return x != null && (i = x[iteratorSymbol]) && i.apply(x); +} : function () { return null; }; +function delArrayItem(a, x) { + var i = a.indexOf(x); + if (i >= 0) + a.splice(i, 1); + return i >= 0; +} +var NO_CHAR_ARRAY = {}; +function getArrayOf(arrayLike) { + var i, a, x, it; + if (arguments.length === 1) { + if (isArray(arrayLike)) + return arrayLike.slice(); + if (this === NO_CHAR_ARRAY && typeof arrayLike === 'string') + return [arrayLike]; + if ((it = getIteratorOf(arrayLike))) { + a = []; + while ((x = it.next()), !x.done) + a.push(x.value); + return a; + } + if (arrayLike == null) + return [arrayLike]; + i = arrayLike.length; + if (typeof i === 'number') { + a = new Array(i); + while (i--) + a[i] = arrayLike[i]; + return a; + } + return [arrayLike]; + } + i = arguments.length; + a = new Array(i); + while (i--) + a[i] = arguments[i]; + return a; +} +var isAsyncFunction = typeof Symbol !== 'undefined' + ? function (fn) { return fn[Symbol.toStringTag] === 'AsyncFunction'; } + : function () { return false; }; + +var dexieErrorNames = [ + 'Modify', + 'Bulk', + 'OpenFailed', + 'VersionChange', + 'Schema', + 'Upgrade', + 'InvalidTable', + 'MissingAPI', + 'NoSuchDatabase', + 'InvalidArgument', + 'SubTransaction', + 'Unsupported', + 'Internal', + 'DatabaseClosed', + 'PrematureCommit', + 'ForeignAwait' +]; +var idbDomErrorNames = [ + 'Unknown', + 'Constraint', + 'Data', + 'TransactionInactive', + 'ReadOnly', + 'Version', + 'NotFound', + 'InvalidState', + 'InvalidAccess', + 'Abort', + 'Timeout', + 'QuotaExceeded', + 'Syntax', + 'DataClone' +]; +var errorList = dexieErrorNames.concat(idbDomErrorNames); +var defaultTexts = { + VersionChanged: "Database version changed by other database connection", + DatabaseClosed: "Database has been closed", + Abort: "Transaction aborted", + TransactionInactive: "Transaction has already completed or failed", + MissingAPI: "IndexedDB API missing. Please visit https://tinyurl.com/y2uuvskb" +}; +function DexieError(name, msg) { + this.name = name; + this.message = msg; +} +derive(DexieError).from(Error).extend({ + toString: function () { return this.name + ": " + this.message; } +}); +function getMultiErrorMessage(msg, failures) { + return msg + ". Errors: " + Object.keys(failures) + .map(function (key) { return failures[key].toString(); }) + .filter(function (v, i, s) { return s.indexOf(v) === i; }) + .join('\n'); +} +function ModifyError(msg, failures, successCount, failedKeys) { + this.failures = failures; + this.failedKeys = failedKeys; + this.successCount = successCount; + this.message = getMultiErrorMessage(msg, failures); +} +derive(ModifyError).from(DexieError); +function BulkError(msg, failures) { + this.name = "BulkError"; + this.failures = Object.keys(failures).map(function (pos) { return failures[pos]; }); + this.failuresByPos = failures; + this.message = getMultiErrorMessage(msg, this.failures); +} +derive(BulkError).from(DexieError); +var errnames = errorList.reduce(function (obj, name) { return (obj[name] = name + "Error", obj); }, {}); +var BaseException = DexieError; +var exceptions = errorList.reduce(function (obj, name) { + var fullName = name + "Error"; + function DexieError(msgOrInner, inner) { + this.name = fullName; + if (!msgOrInner) { + this.message = defaultTexts[name] || fullName; + this.inner = null; + } + else if (typeof msgOrInner === 'string') { + this.message = "".concat(msgOrInner).concat(!inner ? '' : '\n ' + inner); + this.inner = inner || null; + } + else if (typeof msgOrInner === 'object') { + this.message = "".concat(msgOrInner.name, " ").concat(msgOrInner.message); + this.inner = msgOrInner; + } + } + derive(DexieError).from(BaseException); + obj[name] = DexieError; + return obj; +}, {}); +exceptions.Syntax = SyntaxError; +exceptions.Type = TypeError; +exceptions.Range = RangeError; +var exceptionMap = idbDomErrorNames.reduce(function (obj, name) { + obj[name + "Error"] = exceptions[name]; + return obj; +}, {}); +function mapError(domError, message) { + if (!domError || domError instanceof DexieError || domError instanceof TypeError || domError instanceof SyntaxError || !domError.name || !exceptionMap[domError.name]) + return domError; + var rv = new exceptionMap[domError.name](message || domError.message, domError); + if ("stack" in domError) { + setProp(rv, "stack", { get: function () { + return this.inner.stack; + } }); + } + return rv; +} +var fullNameExceptions = errorList.reduce(function (obj, name) { + if (["Syntax", "Type", "Range"].indexOf(name) === -1) + obj[name + "Error"] = exceptions[name]; + return obj; +}, {}); +fullNameExceptions.ModifyError = ModifyError; +fullNameExceptions.DexieError = DexieError; +fullNameExceptions.BulkError = BulkError; + +function nop() { } +function mirror(val) { return val; } +function pureFunctionChain(f1, f2) { + if (f1 == null || f1 === mirror) + return f2; + return function (val) { + return f2(f1(val)); + }; +} +function callBoth(on1, on2) { + return function () { + on1.apply(this, arguments); + on2.apply(this, arguments); + }; +} +function hookCreatingChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + var res = f1.apply(this, arguments); + if (res !== undefined) + arguments[0] = res; + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = null; + this.onerror = null; + var res2 = f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + return res2 !== undefined ? res2 : res; + }; +} +function hookDeletingChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + f1.apply(this, arguments); + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = this.onerror = null; + f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + }; +} +function hookUpdatingChain(f1, f2) { + if (f1 === nop) + return f2; + return function (modifications) { + var res = f1.apply(this, arguments); + extend(modifications, res); + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = null; + this.onerror = null; + var res2 = f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + return res === undefined ? + (res2 === undefined ? undefined : res2) : + (extend(res, res2)); + }; +} +function reverseStoppableEventChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + if (f2.apply(this, arguments) === false) + return false; + return f1.apply(this, arguments); + }; +} +function promisableChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + var res = f1.apply(this, arguments); + if (res && typeof res.then === 'function') { + var thiz = this, i = arguments.length, args = new Array(i); + while (i--) + args[i] = arguments[i]; + return res.then(function () { + return f2.apply(thiz, args); + }); + } + return f2.apply(this, arguments); + }; +} + +var debug = typeof location !== 'undefined' && + /^(http|https):\/\/(localhost|127\.0\.0\.1)/.test(location.href); +function setDebug(value, filter) { + debug = value; +} + +var INTERNAL = {}; +var ZONE_ECHO_LIMIT = 100, _a$1 = typeof Promise === 'undefined' ? + [] : + (function () { + var globalP = Promise.resolve(); + if (typeof crypto === 'undefined' || !crypto.subtle) + return [globalP, getProto(globalP), globalP]; + var nativeP = crypto.subtle.digest("SHA-512", new Uint8Array([0])); + return [ + nativeP, + getProto(nativeP), + globalP + ]; + })(), resolvedNativePromise = _a$1[0], nativePromiseProto = _a$1[1], resolvedGlobalPromise = _a$1[2], nativePromiseThen = nativePromiseProto && nativePromiseProto.then; +var NativePromise = resolvedNativePromise && resolvedNativePromise.constructor; +var patchGlobalPromise = !!resolvedGlobalPromise; +function schedulePhysicalTick() { + queueMicrotask(physicalTick); +} +var asap = function (callback, args) { + microtickQueue.push([callback, args]); + if (needsNewPhysicalTick) { + schedulePhysicalTick(); + needsNewPhysicalTick = false; + } +}; +var isOutsideMicroTick = true, +needsNewPhysicalTick = true, +unhandledErrors = [], +rejectingErrors = [], +rejectionMapper = mirror; +var globalPSD = { + id: 'global', + global: true, + ref: 0, + unhandleds: [], + onunhandled: nop, + pgp: false, + env: {}, + finalize: nop +}; +var PSD = globalPSD; +var microtickQueue = []; +var numScheduledCalls = 0; +var tickFinalizers = []; +function DexiePromise(fn) { + if (typeof this !== 'object') + throw new TypeError('Promises must be constructed via new'); + this._listeners = []; + this._lib = false; + var psd = (this._PSD = PSD); + if (typeof fn !== 'function') { + if (fn !== INTERNAL) + throw new TypeError('Not a function'); + this._state = arguments[1]; + this._value = arguments[2]; + if (this._state === false) + handleRejection(this, this._value); + return; + } + this._state = null; + this._value = null; + ++psd.ref; + executePromiseTask(this, fn); +} +var thenProp = { + get: function () { + var psd = PSD, microTaskId = totalEchoes; + function then(onFulfilled, onRejected) { + var _this = this; + var possibleAwait = !psd.global && (psd !== PSD || microTaskId !== totalEchoes); + var cleanup = possibleAwait && !decrementExpectedAwaits(); + var rv = new DexiePromise(function (resolve, reject) { + propagateToListener(_this, new Listener(nativeAwaitCompatibleWrap(onFulfilled, psd, possibleAwait, cleanup), nativeAwaitCompatibleWrap(onRejected, psd, possibleAwait, cleanup), resolve, reject, psd)); + }); + if (this._consoleTask) + rv._consoleTask = this._consoleTask; + return rv; + } + then.prototype = INTERNAL; + return then; + }, + set: function (value) { + setProp(this, 'then', value && value.prototype === INTERNAL ? + thenProp : + { + get: function () { + return value; + }, + set: thenProp.set + }); + } +}; +props(DexiePromise.prototype, { + then: thenProp, + _then: function (onFulfilled, onRejected) { + propagateToListener(this, new Listener(null, null, onFulfilled, onRejected, PSD)); + }, + catch: function (onRejected) { + if (arguments.length === 1) + return this.then(null, onRejected); + var type = arguments[0], handler = arguments[1]; + return typeof type === 'function' ? this.then(null, function (err) { + return err instanceof type ? handler(err) : PromiseReject(err); + }) + : this.then(null, function (err) { + return err && err.name === type ? handler(err) : PromiseReject(err); + }); + }, + finally: function (onFinally) { + return this.then(function (value) { + return DexiePromise.resolve(onFinally()).then(function () { return value; }); + }, function (err) { + return DexiePromise.resolve(onFinally()).then(function () { return PromiseReject(err); }); + }); + }, + timeout: function (ms, msg) { + var _this = this; + return ms < Infinity ? + new DexiePromise(function (resolve, reject) { + var handle = setTimeout(function () { return reject(new exceptions.Timeout(msg)); }, ms); + _this.then(resolve, reject).finally(clearTimeout.bind(null, handle)); + }) : this; + } +}); +if (typeof Symbol !== 'undefined' && Symbol.toStringTag) + setProp(DexiePromise.prototype, Symbol.toStringTag, 'Dexie.Promise'); +globalPSD.env = snapShot(); +function Listener(onFulfilled, onRejected, resolve, reject, zone) { + this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; + this.onRejected = typeof onRejected === 'function' ? onRejected : null; + this.resolve = resolve; + this.reject = reject; + this.psd = zone; +} +props(DexiePromise, { + all: function () { + var values = getArrayOf.apply(null, arguments) + .map(onPossibleParallellAsync); + return new DexiePromise(function (resolve, reject) { + if (values.length === 0) + resolve([]); + var remaining = values.length; + values.forEach(function (a, i) { return DexiePromise.resolve(a).then(function (x) { + values[i] = x; + if (!--remaining) + resolve(values); + }, reject); }); + }); + }, + resolve: function (value) { + if (value instanceof DexiePromise) + return value; + if (value && typeof value.then === 'function') + return new DexiePromise(function (resolve, reject) { + value.then(resolve, reject); + }); + var rv = new DexiePromise(INTERNAL, true, value); + return rv; + }, + reject: PromiseReject, + race: function () { + var values = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise(function (resolve, reject) { + values.map(function (value) { return DexiePromise.resolve(value).then(resolve, reject); }); + }); + }, + PSD: { + get: function () { return PSD; }, + set: function (value) { return PSD = value; } + }, + totalEchoes: { get: function () { return totalEchoes; } }, + newPSD: newScope, + usePSD: usePSD, + scheduler: { + get: function () { return asap; }, + set: function (value) { asap = value; } + }, + rejectionMapper: { + get: function () { return rejectionMapper; }, + set: function (value) { rejectionMapper = value; } + }, + follow: function (fn, zoneProps) { + return new DexiePromise(function (resolve, reject) { + return newScope(function (resolve, reject) { + var psd = PSD; + psd.unhandleds = []; + psd.onunhandled = reject; + psd.finalize = callBoth(function () { + var _this = this; + run_at_end_of_this_or_next_physical_tick(function () { + _this.unhandleds.length === 0 ? resolve() : reject(_this.unhandleds[0]); + }); + }, psd.finalize); + fn(); + }, zoneProps, resolve, reject); + }); + } +}); +if (NativePromise) { + if (NativePromise.allSettled) + setProp(DexiePromise, "allSettled", function () { + var possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise(function (resolve) { + if (possiblePromises.length === 0) + resolve([]); + var remaining = possiblePromises.length; + var results = new Array(remaining); + possiblePromises.forEach(function (p, i) { return DexiePromise.resolve(p).then(function (value) { return results[i] = { status: "fulfilled", value: value }; }, function (reason) { return results[i] = { status: "rejected", reason: reason }; }) + .then(function () { return --remaining || resolve(results); }); }); + }); + }); + if (NativePromise.any && typeof AggregateError !== 'undefined') + setProp(DexiePromise, "any", function () { + var possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise(function (resolve, reject) { + if (possiblePromises.length === 0) + reject(new AggregateError([])); + var remaining = possiblePromises.length; + var failures = new Array(remaining); + possiblePromises.forEach(function (p, i) { return DexiePromise.resolve(p).then(function (value) { return resolve(value); }, function (failure) { + failures[i] = failure; + if (!--remaining) + reject(new AggregateError(failures)); + }); }); + }); + }); + if (NativePromise.withResolvers) + DexiePromise.withResolvers = NativePromise.withResolvers; +} +function executePromiseTask(promise, fn) { + try { + fn(function (value) { + if (promise._state !== null) + return; + if (value === promise) + throw new TypeError('A promise cannot be resolved with itself.'); + var shouldExecuteTick = promise._lib && beginMicroTickScope(); + if (value && typeof value.then === 'function') { + executePromiseTask(promise, function (resolve, reject) { + value instanceof DexiePromise ? + value._then(resolve, reject) : + value.then(resolve, reject); + }); + } + else { + promise._state = true; + promise._value = value; + propagateAllListeners(promise); + } + if (shouldExecuteTick) + endMicroTickScope(); + }, handleRejection.bind(null, promise)); + } + catch (ex) { + handleRejection(promise, ex); + } +} +function handleRejection(promise, reason) { + rejectingErrors.push(reason); + if (promise._state !== null) + return; + var shouldExecuteTick = promise._lib && beginMicroTickScope(); + reason = rejectionMapper(reason); + promise._state = false; + promise._value = reason; + addPossiblyUnhandledError(promise); + propagateAllListeners(promise); + if (shouldExecuteTick) + endMicroTickScope(); +} +function propagateAllListeners(promise) { + var listeners = promise._listeners; + promise._listeners = []; + for (var i = 0, len = listeners.length; i < len; ++i) { + propagateToListener(promise, listeners[i]); + } + var psd = promise._PSD; + --psd.ref || psd.finalize(); + if (numScheduledCalls === 0) { + ++numScheduledCalls; + asap(function () { + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + }, []); + } +} +function propagateToListener(promise, listener) { + if (promise._state === null) { + promise._listeners.push(listener); + return; + } + var cb = promise._state ? listener.onFulfilled : listener.onRejected; + if (cb === null) { + return (promise._state ? listener.resolve : listener.reject)(promise._value); + } + ++listener.psd.ref; + ++numScheduledCalls; + asap(callListener, [cb, promise, listener]); +} +function callListener(cb, promise, listener) { + try { + var ret, value = promise._value; + if (!promise._state && rejectingErrors.length) + rejectingErrors = []; + ret = debug && promise._consoleTask ? promise._consoleTask.run(function () { return cb(value); }) : cb(value); + if (!promise._state && rejectingErrors.indexOf(value) === -1) { + markErrorAsHandled(promise); + } + listener.resolve(ret); + } + catch (e) { + listener.reject(e); + } + finally { + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + --listener.psd.ref || listener.psd.finalize(); + } +} +function physicalTick() { + usePSD(globalPSD, function () { + beginMicroTickScope() && endMicroTickScope(); + }); +} +function beginMicroTickScope() { + var wasRootExec = isOutsideMicroTick; + isOutsideMicroTick = false; + needsNewPhysicalTick = false; + return wasRootExec; +} +function endMicroTickScope() { + var callbacks, i, l; + do { + while (microtickQueue.length > 0) { + callbacks = microtickQueue; + microtickQueue = []; + l = callbacks.length; + for (i = 0; i < l; ++i) { + var item = callbacks[i]; + item[0].apply(null, item[1]); + } + } + } while (microtickQueue.length > 0); + isOutsideMicroTick = true; + needsNewPhysicalTick = true; +} +function finalizePhysicalTick() { + var unhandledErrs = unhandledErrors; + unhandledErrors = []; + unhandledErrs.forEach(function (p) { + p._PSD.onunhandled.call(null, p._value, p); + }); + var finalizers = tickFinalizers.slice(0); + var i = finalizers.length; + while (i) + finalizers[--i](); +} +function run_at_end_of_this_or_next_physical_tick(fn) { + function finalizer() { + fn(); + tickFinalizers.splice(tickFinalizers.indexOf(finalizer), 1); + } + tickFinalizers.push(finalizer); + ++numScheduledCalls; + asap(function () { + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + }, []); +} +function addPossiblyUnhandledError(promise) { + if (!unhandledErrors.some(function (p) { return p._value === promise._value; })) + unhandledErrors.push(promise); +} +function markErrorAsHandled(promise) { + var i = unhandledErrors.length; + while (i) + if (unhandledErrors[--i]._value === promise._value) { + unhandledErrors.splice(i, 1); + return; + } +} +function PromiseReject(reason) { + return new DexiePromise(INTERNAL, false, reason); +} +function wrap(fn, errorCatcher) { + var psd = PSD; + return function () { + var wasRootExec = beginMicroTickScope(), outerScope = PSD; + try { + switchToZone(psd, true); + return fn.apply(this, arguments); + } + catch (e) { + errorCatcher && errorCatcher(e); + } + finally { + switchToZone(outerScope, false); + if (wasRootExec) + endMicroTickScope(); + } + }; +} +var task = { awaits: 0, echoes: 0, id: 0 }; +var taskCounter = 0; +var zoneStack = []; +var zoneEchoes = 0; +var totalEchoes = 0; +var zone_id_counter = 0; +function newScope(fn, props, a1, a2) { + var parent = PSD, psd = Object.create(parent); + psd.parent = parent; + psd.ref = 0; + psd.global = false; + psd.id = ++zone_id_counter; + globalPSD.env; + psd.env = patchGlobalPromise ? { + Promise: DexiePromise, + PromiseProp: { value: DexiePromise, configurable: true, writable: true }, + all: DexiePromise.all, + race: DexiePromise.race, + allSettled: DexiePromise.allSettled, + any: DexiePromise.any, + resolve: DexiePromise.resolve, + reject: DexiePromise.reject, + } : {}; + if (props) + extend(psd, props); + ++parent.ref; + psd.finalize = function () { + --this.parent.ref || this.parent.finalize(); + }; + var rv = usePSD(psd, fn, a1, a2); + if (psd.ref === 0) + psd.finalize(); + return rv; +} +function incrementExpectedAwaits() { + if (!task.id) + task.id = ++taskCounter; + ++task.awaits; + task.echoes += ZONE_ECHO_LIMIT; + return task.id; +} +function decrementExpectedAwaits() { + if (!task.awaits) + return false; + if (--task.awaits === 0) + task.id = 0; + task.echoes = task.awaits * ZONE_ECHO_LIMIT; + return true; +} +if (('' + nativePromiseThen).indexOf('[native code]') === -1) { + incrementExpectedAwaits = decrementExpectedAwaits = nop; +} +function onPossibleParallellAsync(possiblePromise) { + if (task.echoes && possiblePromise && possiblePromise.constructor === NativePromise) { + incrementExpectedAwaits(); + return possiblePromise.then(function (x) { + decrementExpectedAwaits(); + return x; + }, function (e) { + decrementExpectedAwaits(); + return rejection(e); + }); + } + return possiblePromise; +} +function zoneEnterEcho(targetZone) { + ++totalEchoes; + if (!task.echoes || --task.echoes === 0) { + task.echoes = task.awaits = task.id = 0; + } + zoneStack.push(PSD); + switchToZone(targetZone, true); +} +function zoneLeaveEcho() { + var zone = zoneStack[zoneStack.length - 1]; + zoneStack.pop(); + switchToZone(zone, false); +} +function switchToZone(targetZone, bEnteringZone) { + var currentZone = PSD; + if (bEnteringZone ? task.echoes && (!zoneEchoes++ || targetZone !== PSD) : zoneEchoes && (!--zoneEchoes || targetZone !== PSD)) { + queueMicrotask(bEnteringZone ? zoneEnterEcho.bind(null, targetZone) : zoneLeaveEcho); + } + if (targetZone === PSD) + return; + PSD = targetZone; + if (currentZone === globalPSD) + globalPSD.env = snapShot(); + if (patchGlobalPromise) { + var GlobalPromise = globalPSD.env.Promise; + var targetEnv = targetZone.env; + if (currentZone.global || targetZone.global) { + Object.defineProperty(_global, 'Promise', targetEnv.PromiseProp); + GlobalPromise.all = targetEnv.all; + GlobalPromise.race = targetEnv.race; + GlobalPromise.resolve = targetEnv.resolve; + GlobalPromise.reject = targetEnv.reject; + if (targetEnv.allSettled) + GlobalPromise.allSettled = targetEnv.allSettled; + if (targetEnv.any) + GlobalPromise.any = targetEnv.any; + } + } +} +function snapShot() { + var GlobalPromise = _global.Promise; + return patchGlobalPromise ? { + Promise: GlobalPromise, + PromiseProp: Object.getOwnPropertyDescriptor(_global, "Promise"), + all: GlobalPromise.all, + race: GlobalPromise.race, + allSettled: GlobalPromise.allSettled, + any: GlobalPromise.any, + resolve: GlobalPromise.resolve, + reject: GlobalPromise.reject, + } : {}; +} +function usePSD(psd, fn, a1, a2, a3) { + var outerScope = PSD; + try { + switchToZone(psd, true); + return fn(a1, a2, a3); + } + finally { + switchToZone(outerScope, false); + } +} +function nativeAwaitCompatibleWrap(fn, zone, possibleAwait, cleanup) { + return typeof fn !== 'function' ? fn : function () { + var outerZone = PSD; + if (possibleAwait) + incrementExpectedAwaits(); + switchToZone(zone, true); + try { + return fn.apply(this, arguments); + } + finally { + switchToZone(outerZone, false); + if (cleanup) + queueMicrotask(decrementExpectedAwaits); + } + }; +} +function execInGlobalContext(cb) { + if (Promise === NativePromise && task.echoes === 0) { + if (zoneEchoes === 0) { + cb(); + } + else { + enqueueNativeMicroTask(cb); + } + } + else { + setTimeout(cb, 0); + } +} +var rejection = DexiePromise.reject; + +function tempTransaction(db, mode, storeNames, fn) { + if (!db.idbdb || (!db._state.openComplete && (!PSD.letThrough && !db._vip))) { + if (db._state.openComplete) { + return rejection(new exceptions.DatabaseClosed(db._state.dbOpenError)); + } + if (!db._state.isBeingOpened) { + if (!db._state.autoOpen) + return rejection(new exceptions.DatabaseClosed()); + db.open().catch(nop); + } + return db._state.dbReadyPromise.then(function () { return tempTransaction(db, mode, storeNames, fn); }); + } + else { + var trans = db._createTransaction(mode, storeNames, db._dbSchema); + try { + trans.create(); + db._state.PR1398_maxLoop = 3; + } + catch (ex) { + if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) { + console.warn('Dexie: Need to reopen db'); + db.close({ disableAutoOpen: false }); + return db.open().then(function () { return tempTransaction(db, mode, storeNames, fn); }); + } + return rejection(ex); + } + return trans._promise(mode, function (resolve, reject) { + return newScope(function () { + PSD.trans = trans; + return fn(resolve, reject, trans); + }); + }).then(function (result) { + if (mode === 'readwrite') + try { + trans.idbtrans.commit(); + } + catch (_a) { } + return mode === 'readonly' ? result : trans._completion.then(function () { return result; }); + }); + } +} + +var DEXIE_VERSION = '4.0.10'; +var maxString = String.fromCharCode(65535); +var minKey = -Infinity; +var INVALID_KEY_ARGUMENT = "Invalid key provided. Keys must be of type string, number, Date or Array."; +var STRING_EXPECTED = "String expected."; +var connections = []; +var DBNAMES_DB = '__dbnames'; +var READONLY = 'readonly'; +var READWRITE = 'readwrite'; + +function combine(filter1, filter2) { + return filter1 ? + filter2 ? + function () { return filter1.apply(this, arguments) && filter2.apply(this, arguments); } : + filter1 : + filter2; +} + +var AnyRange = { + type: 3 , + lower: -Infinity, + lowerOpen: false, + upper: [[]], + upperOpen: false +}; + +function workaroundForUndefinedPrimKey(keyPath) { + return typeof keyPath === "string" && !/\./.test(keyPath) + ? function (obj) { + if (obj[keyPath] === undefined && (keyPath in obj)) { + obj = deepClone(obj); + delete obj[keyPath]; + } + return obj; + } + : function (obj) { return obj; }; +} + +function Entity() { + throw exceptions.Type(); +} + +function cmp(a, b) { + try { + var ta = type(a); + var tb = type(b); + if (ta !== tb) { + if (ta === 'Array') + return 1; + if (tb === 'Array') + return -1; + if (ta === 'binary') + return 1; + if (tb === 'binary') + return -1; + if (ta === 'string') + return 1; + if (tb === 'string') + return -1; + if (ta === 'Date') + return 1; + if (tb !== 'Date') + return NaN; + return -1; + } + switch (ta) { + case 'number': + case 'Date': + case 'string': + return a > b ? 1 : a < b ? -1 : 0; + case 'binary': { + return compareUint8Arrays(getUint8Array(a), getUint8Array(b)); + } + case 'Array': + return compareArrays(a, b); + } + } + catch (_a) { } + return NaN; +} +function compareArrays(a, b) { + var al = a.length; + var bl = b.length; + var l = al < bl ? al : bl; + for (var i = 0; i < l; ++i) { + var res = cmp(a[i], b[i]); + if (res !== 0) + return res; + } + return al === bl ? 0 : al < bl ? -1 : 1; +} +function compareUint8Arrays(a, b) { + var al = a.length; + var bl = b.length; + var l = al < bl ? al : bl; + for (var i = 0; i < l; ++i) { + if (a[i] !== b[i]) + return a[i] < b[i] ? -1 : 1; + } + return al === bl ? 0 : al < bl ? -1 : 1; +} +function type(x) { + var t = typeof x; + if (t !== 'object') + return t; + if (ArrayBuffer.isView(x)) + return 'binary'; + var tsTag = toStringTag(x); + return tsTag === 'ArrayBuffer' ? 'binary' : tsTag; +} +function getUint8Array(a) { + if (a instanceof Uint8Array) + return a; + if (ArrayBuffer.isView(a)) + return new Uint8Array(a.buffer, a.byteOffset, a.byteLength); + return new Uint8Array(a); +} + +var Table = (function () { + function Table() { + } + Table.prototype._trans = function (mode, fn, writeLocked) { + var trans = this._tx || PSD.trans; + var tableName = this.name; + var task = debug && typeof console !== 'undefined' && console.createTask && console.createTask("Dexie: ".concat(mode === 'readonly' ? 'read' : 'write', " ").concat(this.name)); + function checkTableInTransaction(resolve, reject, trans) { + if (!trans.schema[tableName]) + throw new exceptions.NotFound("Table " + tableName + " not part of transaction"); + return fn(trans.idbtrans, trans); + } + var wasRootExec = beginMicroTickScope(); + try { + var p = trans && trans.db._novip === this.db._novip ? + trans === PSD.trans ? + trans._promise(mode, checkTableInTransaction, writeLocked) : + newScope(function () { return trans._promise(mode, checkTableInTransaction, writeLocked); }, { trans: trans, transless: PSD.transless || PSD }) : + tempTransaction(this.db, mode, [this.name], checkTableInTransaction); + if (task) { + p._consoleTask = task; + p = p.catch(function (err) { + console.trace(err); + return rejection(err); + }); + } + return p; + } + finally { + if (wasRootExec) + endMicroTickScope(); + } + }; + Table.prototype.get = function (keyOrCrit, cb) { + var _this = this; + if (keyOrCrit && keyOrCrit.constructor === Object) + return this.where(keyOrCrit).first(cb); + if (keyOrCrit == null) + return rejection(new exceptions.Type("Invalid argument to Table.get()")); + return this._trans('readonly', function (trans) { + return _this.core.get({ trans: trans, key: keyOrCrit }) + .then(function (res) { return _this.hook.reading.fire(res); }); + }).then(cb); + }; + Table.prototype.where = function (indexOrCrit) { + if (typeof indexOrCrit === 'string') + return new this.db.WhereClause(this, indexOrCrit); + if (isArray(indexOrCrit)) + return new this.db.WhereClause(this, "[".concat(indexOrCrit.join('+'), "]")); + var keyPaths = keys(indexOrCrit); + if (keyPaths.length === 1) + return this + .where(keyPaths[0]) + .equals(indexOrCrit[keyPaths[0]]); + var compoundIndex = this.schema.indexes.concat(this.schema.primKey).filter(function (ix) { + if (ix.compound && + keyPaths.every(function (keyPath) { return ix.keyPath.indexOf(keyPath) >= 0; })) { + for (var i = 0; i < keyPaths.length; ++i) { + if (keyPaths.indexOf(ix.keyPath[i]) === -1) + return false; + } + return true; + } + return false; + }).sort(function (a, b) { return a.keyPath.length - b.keyPath.length; })[0]; + if (compoundIndex && this.db._maxKey !== maxString) { + var keyPathsInValidOrder = compoundIndex.keyPath.slice(0, keyPaths.length); + return this + .where(keyPathsInValidOrder) + .equals(keyPathsInValidOrder.map(function (kp) { return indexOrCrit[kp]; })); + } + if (!compoundIndex && debug) + console.warn("The query ".concat(JSON.stringify(indexOrCrit), " on ").concat(this.name, " would benefit from a ") + + "compound index [".concat(keyPaths.join('+'), "]")); + var idxByName = this.schema.idxByName; + function equals(a, b) { + return cmp(a, b) === 0; + } + var _a = keyPaths.reduce(function (_a, keyPath) { + var prevIndex = _a[0], prevFilterFn = _a[1]; + var index = idxByName[keyPath]; + var value = indexOrCrit[keyPath]; + return [ + prevIndex || index, + prevIndex || !index ? + combine(prevFilterFn, index && index.multi ? + function (x) { + var prop = getByKeyPath(x, keyPath); + return isArray(prop) && prop.some(function (item) { return equals(value, item); }); + } : function (x) { return equals(value, getByKeyPath(x, keyPath)); }) + : prevFilterFn + ]; + }, [null, null]), idx = _a[0], filterFunction = _a[1]; + return idx ? + this.where(idx.name).equals(indexOrCrit[idx.keyPath]) + .filter(filterFunction) : + compoundIndex ? + this.filter(filterFunction) : + this.where(keyPaths).equals(''); + }; + Table.prototype.filter = function (filterFunction) { + return this.toCollection().and(filterFunction); + }; + Table.prototype.count = function (thenShortcut) { + return this.toCollection().count(thenShortcut); + }; + Table.prototype.offset = function (offset) { + return this.toCollection().offset(offset); + }; + Table.prototype.limit = function (numRows) { + return this.toCollection().limit(numRows); + }; + Table.prototype.each = function (callback) { + return this.toCollection().each(callback); + }; + Table.prototype.toArray = function (thenShortcut) { + return this.toCollection().toArray(thenShortcut); + }; + Table.prototype.toCollection = function () { + return new this.db.Collection(new this.db.WhereClause(this)); + }; + Table.prototype.orderBy = function (index) { + return new this.db.Collection(new this.db.WhereClause(this, isArray(index) ? + "[".concat(index.join('+'), "]") : + index)); + }; + Table.prototype.reverse = function () { + return this.toCollection().reverse(); + }; + Table.prototype.mapToClass = function (constructor) { + var _a = this, db = _a.db, tableName = _a.name; + this.schema.mappedClass = constructor; + if (constructor.prototype instanceof Entity) { + constructor = (function (_super) { + __extends(class_1, _super); + function class_1() { + return _super !== null && _super.apply(this, arguments) || this; + } + Object.defineProperty(class_1.prototype, "db", { + get: function () { return db; }, + enumerable: false, + configurable: true + }); + class_1.prototype.table = function () { return tableName; }; + return class_1; + }(constructor)); + } + var inheritedProps = new Set(); + for (var proto = constructor.prototype; proto; proto = getProto(proto)) { + Object.getOwnPropertyNames(proto).forEach(function (propName) { return inheritedProps.add(propName); }); + } + var readHook = function (obj) { + if (!obj) + return obj; + var res = Object.create(constructor.prototype); + for (var m in obj) + if (!inheritedProps.has(m)) + try { + res[m] = obj[m]; + } + catch (_) { } + return res; + }; + if (this.schema.readHook) { + this.hook.reading.unsubscribe(this.schema.readHook); + } + this.schema.readHook = readHook; + this.hook("reading", readHook); + return constructor; + }; + Table.prototype.defineClass = function () { + function Class(content) { + extend(this, content); + } + return this.mapToClass(Class); + }; + Table.prototype.add = function (obj, key) { + var _this = this; + var _a = this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath; + var objToAdd = obj; + if (keyPath && auto) { + objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj); + } + return this._trans('readwrite', function (trans) { + return _this.core.mutate({ trans: trans, type: 'add', keys: key != null ? [key] : null, values: [objToAdd] }); + }).then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult; }) + .then(function (lastResult) { + if (keyPath) { + try { + setByKeyPath(obj, keyPath, lastResult); + } + catch (_) { } + } + return lastResult; + }); + }; + Table.prototype.update = function (keyOrObject, modifications) { + if (typeof keyOrObject === 'object' && !isArray(keyOrObject)) { + var key = getByKeyPath(keyOrObject, this.schema.primKey.keyPath); + if (key === undefined) + return rejection(new exceptions.InvalidArgument("Given object does not contain its primary key")); + return this.where(":id").equals(key).modify(modifications); + } + else { + return this.where(":id").equals(keyOrObject).modify(modifications); + } + }; + Table.prototype.put = function (obj, key) { + var _this = this; + var _a = this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath; + var objToAdd = obj; + if (keyPath && auto) { + objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj); + } + return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'put', values: [objToAdd], keys: key != null ? [key] : null }); }) + .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult; }) + .then(function (lastResult) { + if (keyPath) { + try { + setByKeyPath(obj, keyPath, lastResult); + } + catch (_) { } + } + return lastResult; + }); + }; + Table.prototype.delete = function (key) { + var _this = this; + return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'delete', keys: [key] }); }) + .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined; }); + }; + Table.prototype.clear = function () { + var _this = this; + return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'deleteRange', range: AnyRange }); }) + .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined; }); + }; + Table.prototype.bulkGet = function (keys) { + var _this = this; + return this._trans('readonly', function (trans) { + return _this.core.getMany({ + keys: keys, + trans: trans + }).then(function (result) { return result.map(function (res) { return _this.hook.reading.fire(res); }); }); + }); + }; + Table.prototype.bulkAdd = function (objects, keysOrOptions, options) { + var _this = this; + var keys = Array.isArray(keysOrOptions) ? keysOrOptions : undefined; + options = options || (keys ? undefined : keysOrOptions); + var wantResults = options ? options.allKeys : undefined; + return this._trans('readwrite', function (trans) { + var _a = _this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath; + if (keyPath && keys) + throw new exceptions.InvalidArgument("bulkAdd(): keys argument invalid on tables with inbound keys"); + if (keys && keys.length !== objects.length) + throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length"); + var numObjects = objects.length; + var objectsToAdd = keyPath && auto ? + objects.map(workaroundForUndefinedPrimKey(keyPath)) : + objects; + return _this.core.mutate({ trans: trans, type: 'add', keys: keys, values: objectsToAdd, wantResults: wantResults }) + .then(function (_a) { + var numFailures = _a.numFailures, results = _a.results, lastResult = _a.lastResult, failures = _a.failures; + var result = wantResults ? results : lastResult; + if (numFailures === 0) + return result; + throw new BulkError("".concat(_this.name, ".bulkAdd(): ").concat(numFailures, " of ").concat(numObjects, " operations failed"), failures); + }); + }); + }; + Table.prototype.bulkPut = function (objects, keysOrOptions, options) { + var _this = this; + var keys = Array.isArray(keysOrOptions) ? keysOrOptions : undefined; + options = options || (keys ? undefined : keysOrOptions); + var wantResults = options ? options.allKeys : undefined; + return this._trans('readwrite', function (trans) { + var _a = _this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath; + if (keyPath && keys) + throw new exceptions.InvalidArgument("bulkPut(): keys argument invalid on tables with inbound keys"); + if (keys && keys.length !== objects.length) + throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length"); + var numObjects = objects.length; + var objectsToPut = keyPath && auto ? + objects.map(workaroundForUndefinedPrimKey(keyPath)) : + objects; + return _this.core.mutate({ trans: trans, type: 'put', keys: keys, values: objectsToPut, wantResults: wantResults }) + .then(function (_a) { + var numFailures = _a.numFailures, results = _a.results, lastResult = _a.lastResult, failures = _a.failures; + var result = wantResults ? results : lastResult; + if (numFailures === 0) + return result; + throw new BulkError("".concat(_this.name, ".bulkPut(): ").concat(numFailures, " of ").concat(numObjects, " operations failed"), failures); + }); + }); + }; + Table.prototype.bulkUpdate = function (keysAndChanges) { + var _this = this; + var coreTable = this.core; + var keys = keysAndChanges.map(function (entry) { return entry.key; }); + var changeSpecs = keysAndChanges.map(function (entry) { return entry.changes; }); + var offsetMap = []; + return this._trans('readwrite', function (trans) { + return coreTable.getMany({ trans: trans, keys: keys, cache: 'clone' }).then(function (objs) { + var resultKeys = []; + var resultObjs = []; + keysAndChanges.forEach(function (_a, idx) { + var key = _a.key, changes = _a.changes; + var obj = objs[idx]; + if (obj) { + for (var _i = 0, _b = Object.keys(changes); _i < _b.length; _i++) { + var keyPath = _b[_i]; + var value = changes[keyPath]; + if (keyPath === _this.schema.primKey.keyPath) { + if (cmp(value, key) !== 0) { + throw new exceptions.Constraint("Cannot update primary key in bulkUpdate()"); + } + } + else { + setByKeyPath(obj, keyPath, value); + } + } + offsetMap.push(idx); + resultKeys.push(key); + resultObjs.push(obj); + } + }); + var numEntries = resultKeys.length; + return coreTable + .mutate({ + trans: trans, + type: 'put', + keys: resultKeys, + values: resultObjs, + updates: { + keys: keys, + changeSpecs: changeSpecs + } + }) + .then(function (_a) { + var numFailures = _a.numFailures, failures = _a.failures; + if (numFailures === 0) + return numEntries; + for (var _i = 0, _b = Object.keys(failures); _i < _b.length; _i++) { + var offset = _b[_i]; + var mappedOffset = offsetMap[Number(offset)]; + if (mappedOffset != null) { + var failure = failures[offset]; + delete failures[offset]; + failures[mappedOffset] = failure; + } + } + throw new BulkError("".concat(_this.name, ".bulkUpdate(): ").concat(numFailures, " of ").concat(numEntries, " operations failed"), failures); + }); + }); + }); + }; + Table.prototype.bulkDelete = function (keys) { + var _this = this; + var numKeys = keys.length; + return this._trans('readwrite', function (trans) { + return _this.core.mutate({ trans: trans, type: 'delete', keys: keys }); + }).then(function (_a) { + var numFailures = _a.numFailures, lastResult = _a.lastResult, failures = _a.failures; + if (numFailures === 0) + return lastResult; + throw new BulkError("".concat(_this.name, ".bulkDelete(): ").concat(numFailures, " of ").concat(numKeys, " operations failed"), failures); + }); + }; + return Table; +}()); + +function Events(ctx) { + var evs = {}; + var rv = function (eventName, subscriber) { + if (subscriber) { + var i = arguments.length, args = new Array(i - 1); + while (--i) + args[i - 1] = arguments[i]; + evs[eventName].subscribe.apply(null, args); + return ctx; + } + else if (typeof (eventName) === 'string') { + return evs[eventName]; + } + }; + rv.addEventType = add; + for (var i = 1, l = arguments.length; i < l; ++i) { + add(arguments[i]); + } + return rv; + function add(eventName, chainFunction, defaultFunction) { + if (typeof eventName === 'object') + return addConfiguredEvents(eventName); + if (!chainFunction) + chainFunction = reverseStoppableEventChain; + if (!defaultFunction) + defaultFunction = nop; + var context = { + subscribers: [], + fire: defaultFunction, + subscribe: function (cb) { + if (context.subscribers.indexOf(cb) === -1) { + context.subscribers.push(cb); + context.fire = chainFunction(context.fire, cb); + } + }, + unsubscribe: function (cb) { + context.subscribers = context.subscribers.filter(function (fn) { return fn !== cb; }); + context.fire = context.subscribers.reduce(chainFunction, defaultFunction); + } + }; + evs[eventName] = rv[eventName] = context; + return context; + } + function addConfiguredEvents(cfg) { + keys(cfg).forEach(function (eventName) { + var args = cfg[eventName]; + if (isArray(args)) { + add(eventName, cfg[eventName][0], cfg[eventName][1]); + } + else if (args === 'asap') { + var context = add(eventName, mirror, function fire() { + var i = arguments.length, args = new Array(i); + while (i--) + args[i] = arguments[i]; + context.subscribers.forEach(function (fn) { + asap$1(function fireEvent() { + fn.apply(null, args); + }); + }); + }); + } + else + throw new exceptions.InvalidArgument("Invalid event config"); + }); + } +} + +function makeClassConstructor(prototype, constructor) { + derive(constructor).from({ prototype: prototype }); + return constructor; +} + +function createTableConstructor(db) { + return makeClassConstructor(Table.prototype, function Table(name, tableSchema, trans) { + this.db = db; + this._tx = trans; + this.name = name; + this.schema = tableSchema; + this.hook = db._allTables[name] ? db._allTables[name].hook : Events(null, { + "creating": [hookCreatingChain, nop], + "reading": [pureFunctionChain, mirror], + "updating": [hookUpdatingChain, nop], + "deleting": [hookDeletingChain, nop] + }); + }); +} + +function isPlainKeyRange(ctx, ignoreLimitFilter) { + return !(ctx.filter || ctx.algorithm || ctx.or) && + (ignoreLimitFilter ? ctx.justLimit : !ctx.replayFilter); +} +function addFilter(ctx, fn) { + ctx.filter = combine(ctx.filter, fn); +} +function addReplayFilter(ctx, factory, isLimitFilter) { + var curr = ctx.replayFilter; + ctx.replayFilter = curr ? function () { return combine(curr(), factory()); } : factory; + ctx.justLimit = isLimitFilter && !curr; +} +function addMatchFilter(ctx, fn) { + ctx.isMatch = combine(ctx.isMatch, fn); +} +function getIndexOrStore(ctx, coreSchema) { + if (ctx.isPrimKey) + return coreSchema.primaryKey; + var index = coreSchema.getIndexByKeyPath(ctx.index); + if (!index) + throw new exceptions.Schema("KeyPath " + ctx.index + " on object store " + coreSchema.name + " is not indexed"); + return index; +} +function openCursor(ctx, coreTable, trans) { + var index = getIndexOrStore(ctx, coreTable.schema); + return coreTable.openCursor({ + trans: trans, + values: !ctx.keysOnly, + reverse: ctx.dir === 'prev', + unique: !!ctx.unique, + query: { + index: index, + range: ctx.range + } + }); +} +function iter(ctx, fn, coreTrans, coreTable) { + var filter = ctx.replayFilter ? combine(ctx.filter, ctx.replayFilter()) : ctx.filter; + if (!ctx.or) { + return iterate(openCursor(ctx, coreTable, coreTrans), combine(ctx.algorithm, filter), fn, !ctx.keysOnly && ctx.valueMapper); + } + else { + var set_1 = {}; + var union = function (item, cursor, advance) { + if (!filter || filter(cursor, advance, function (result) { return cursor.stop(result); }, function (err) { return cursor.fail(err); })) { + var primaryKey = cursor.primaryKey; + var key = '' + primaryKey; + if (key === '[object ArrayBuffer]') + key = '' + new Uint8Array(primaryKey); + if (!hasOwn(set_1, key)) { + set_1[key] = true; + fn(item, cursor, advance); + } + } + }; + return Promise.all([ + ctx.or._iterate(union, coreTrans), + iterate(openCursor(ctx, coreTable, coreTrans), ctx.algorithm, union, !ctx.keysOnly && ctx.valueMapper) + ]); + } +} +function iterate(cursorPromise, filter, fn, valueMapper) { + var mappedFn = valueMapper ? function (x, c, a) { return fn(valueMapper(x), c, a); } : fn; + var wrappedFn = wrap(mappedFn); + return cursorPromise.then(function (cursor) { + if (cursor) { + return cursor.start(function () { + var c = function () { return cursor.continue(); }; + if (!filter || filter(cursor, function (advancer) { return c = advancer; }, function (val) { cursor.stop(val); c = nop; }, function (e) { cursor.fail(e); c = nop; })) + wrappedFn(cursor.value, cursor, function (advancer) { return c = advancer; }); + c(); + }); + } + }); +} + +var PropModSymbol = Symbol(); +var PropModification = (function () { + function PropModification(spec) { + Object.assign(this, spec); + } + PropModification.prototype.execute = function (value) { + var _a; + if (this.add !== undefined) { + var term = this.add; + if (isArray(term)) { + return __spreadArray(__spreadArray([], (isArray(value) ? value : []), true), term, true).sort(); + } + if (typeof term === 'number') + return (Number(value) || 0) + term; + if (typeof term === 'bigint') { + try { + return BigInt(value) + term; + } + catch (_b) { + return BigInt(0) + term; + } + } + throw new TypeError("Invalid term ".concat(term)); + } + if (this.remove !== undefined) { + var subtrahend_1 = this.remove; + if (isArray(subtrahend_1)) { + return isArray(value) ? value.filter(function (item) { return !subtrahend_1.includes(item); }).sort() : []; + } + if (typeof subtrahend_1 === 'number') + return Number(value) - subtrahend_1; + if (typeof subtrahend_1 === 'bigint') { + try { + return BigInt(value) - subtrahend_1; + } + catch (_c) { + return BigInt(0) - subtrahend_1; + } + } + throw new TypeError("Invalid subtrahend ".concat(subtrahend_1)); + } + var prefixToReplace = (_a = this.replacePrefix) === null || _a === void 0 ? void 0 : _a[0]; + if (prefixToReplace && typeof value === 'string' && value.startsWith(prefixToReplace)) { + return this.replacePrefix[1] + value.substring(prefixToReplace.length); + } + return value; + }; + return PropModification; +}()); + +var Collection = (function () { + function Collection() { + } + Collection.prototype._read = function (fn, cb) { + var ctx = this._ctx; + return ctx.error ? + ctx.table._trans(null, rejection.bind(null, ctx.error)) : + ctx.table._trans('readonly', fn).then(cb); + }; + Collection.prototype._write = function (fn) { + var ctx = this._ctx; + return ctx.error ? + ctx.table._trans(null, rejection.bind(null, ctx.error)) : + ctx.table._trans('readwrite', fn, "locked"); + }; + Collection.prototype._addAlgorithm = function (fn) { + var ctx = this._ctx; + ctx.algorithm = combine(ctx.algorithm, fn); + }; + Collection.prototype._iterate = function (fn, coreTrans) { + return iter(this._ctx, fn, coreTrans, this._ctx.table.core); + }; + Collection.prototype.clone = function (props) { + var rv = Object.create(this.constructor.prototype), ctx = Object.create(this._ctx); + if (props) + extend(ctx, props); + rv._ctx = ctx; + return rv; + }; + Collection.prototype.raw = function () { + this._ctx.valueMapper = null; + return this; + }; + Collection.prototype.each = function (fn) { + var ctx = this._ctx; + return this._read(function (trans) { return iter(ctx, fn, trans, ctx.table.core); }); + }; + Collection.prototype.count = function (cb) { + var _this = this; + return this._read(function (trans) { + var ctx = _this._ctx; + var coreTable = ctx.table.core; + if (isPlainKeyRange(ctx, true)) { + return coreTable.count({ + trans: trans, + query: { + index: getIndexOrStore(ctx, coreTable.schema), + range: ctx.range + } + }).then(function (count) { return Math.min(count, ctx.limit); }); + } + else { + var count = 0; + return iter(ctx, function () { ++count; return false; }, trans, coreTable) + .then(function () { return count; }); + } + }).then(cb); + }; + Collection.prototype.sortBy = function (keyPath, cb) { + var parts = keyPath.split('.').reverse(), lastPart = parts[0], lastIndex = parts.length - 1; + function getval(obj, i) { + if (i) + return getval(obj[parts[i]], i - 1); + return obj[lastPart]; + } + var order = this._ctx.dir === "next" ? 1 : -1; + function sorter(a, b) { + var aVal = getval(a, lastIndex), bVal = getval(b, lastIndex); + return cmp(aVal, bVal) * order; + } + return this.toArray(function (a) { + return a.sort(sorter); + }).then(cb); + }; + Collection.prototype.toArray = function (cb) { + var _this = this; + return this._read(function (trans) { + var ctx = _this._ctx; + if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) { + var valueMapper_1 = ctx.valueMapper; + var index = getIndexOrStore(ctx, ctx.table.core.schema); + return ctx.table.core.query({ + trans: trans, + limit: ctx.limit, + values: true, + query: { + index: index, + range: ctx.range + } + }).then(function (_a) { + var result = _a.result; + return valueMapper_1 ? result.map(valueMapper_1) : result; + }); + } + else { + var a_1 = []; + return iter(ctx, function (item) { return a_1.push(item); }, trans, ctx.table.core).then(function () { return a_1; }); + } + }, cb); + }; + Collection.prototype.offset = function (offset) { + var ctx = this._ctx; + if (offset <= 0) + return this; + ctx.offset += offset; + if (isPlainKeyRange(ctx)) { + addReplayFilter(ctx, function () { + var offsetLeft = offset; + return function (cursor, advance) { + if (offsetLeft === 0) + return true; + if (offsetLeft === 1) { + --offsetLeft; + return false; + } + advance(function () { + cursor.advance(offsetLeft); + offsetLeft = 0; + }); + return false; + }; + }); + } + else { + addReplayFilter(ctx, function () { + var offsetLeft = offset; + return function () { return (--offsetLeft < 0); }; + }); + } + return this; + }; + Collection.prototype.limit = function (numRows) { + this._ctx.limit = Math.min(this._ctx.limit, numRows); + addReplayFilter(this._ctx, function () { + var rowsLeft = numRows; + return function (cursor, advance, resolve) { + if (--rowsLeft <= 0) + advance(resolve); + return rowsLeft >= 0; + }; + }, true); + return this; + }; + Collection.prototype.until = function (filterFunction, bIncludeStopEntry) { + addFilter(this._ctx, function (cursor, advance, resolve) { + if (filterFunction(cursor.value)) { + advance(resolve); + return bIncludeStopEntry; + } + else { + return true; + } + }); + return this; + }; + Collection.prototype.first = function (cb) { + return this.limit(1).toArray(function (a) { return a[0]; }).then(cb); + }; + Collection.prototype.last = function (cb) { + return this.reverse().first(cb); + }; + Collection.prototype.filter = function (filterFunction) { + addFilter(this._ctx, function (cursor) { + return filterFunction(cursor.value); + }); + addMatchFilter(this._ctx, filterFunction); + return this; + }; + Collection.prototype.and = function (filter) { + return this.filter(filter); + }; + Collection.prototype.or = function (indexName) { + return new this.db.WhereClause(this._ctx.table, indexName, this); + }; + Collection.prototype.reverse = function () { + this._ctx.dir = (this._ctx.dir === "prev" ? "next" : "prev"); + if (this._ondirectionchange) + this._ondirectionchange(this._ctx.dir); + return this; + }; + Collection.prototype.desc = function () { + return this.reverse(); + }; + Collection.prototype.eachKey = function (cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + return this.each(function (val, cursor) { cb(cursor.key, cursor); }); + }; + Collection.prototype.eachUniqueKey = function (cb) { + this._ctx.unique = "unique"; + return this.eachKey(cb); + }; + Collection.prototype.eachPrimaryKey = function (cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + return this.each(function (val, cursor) { cb(cursor.primaryKey, cursor); }); + }; + Collection.prototype.keys = function (cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + var a = []; + return this.each(function (item, cursor) { + a.push(cursor.key); + }).then(function () { + return a; + }).then(cb); + }; + Collection.prototype.primaryKeys = function (cb) { + var ctx = this._ctx; + if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) { + return this._read(function (trans) { + var index = getIndexOrStore(ctx, ctx.table.core.schema); + return ctx.table.core.query({ + trans: trans, + values: false, + limit: ctx.limit, + query: { + index: index, + range: ctx.range + } + }); + }).then(function (_a) { + var result = _a.result; + return result; + }).then(cb); + } + ctx.keysOnly = !ctx.isMatch; + var a = []; + return this.each(function (item, cursor) { + a.push(cursor.primaryKey); + }).then(function () { + return a; + }).then(cb); + }; + Collection.prototype.uniqueKeys = function (cb) { + this._ctx.unique = "unique"; + return this.keys(cb); + }; + Collection.prototype.firstKey = function (cb) { + return this.limit(1).keys(function (a) { return a[0]; }).then(cb); + }; + Collection.prototype.lastKey = function (cb) { + return this.reverse().firstKey(cb); + }; + Collection.prototype.distinct = function () { + var ctx = this._ctx, idx = ctx.index && ctx.table.schema.idxByName[ctx.index]; + if (!idx || !idx.multi) + return this; + var set = {}; + addFilter(this._ctx, function (cursor) { + var strKey = cursor.primaryKey.toString(); + var found = hasOwn(set, strKey); + set[strKey] = true; + return !found; + }); + return this; + }; + Collection.prototype.modify = function (changes) { + var _this = this; + var ctx = this._ctx; + return this._write(function (trans) { + var modifyer; + if (typeof changes === 'function') { + modifyer = changes; + } + else { + var keyPaths = keys(changes); + var numKeys = keyPaths.length; + modifyer = function (item) { + var anythingModified = false; + for (var i = 0; i < numKeys; ++i) { + var keyPath = keyPaths[i]; + var val = changes[keyPath]; + var origVal = getByKeyPath(item, keyPath); + if (val instanceof PropModification) { + setByKeyPath(item, keyPath, val.execute(origVal)); + anythingModified = true; + } + else if (origVal !== val) { + setByKeyPath(item, keyPath, val); + anythingModified = true; + } + } + return anythingModified; + }; + } + var coreTable = ctx.table.core; + var _a = coreTable.schema.primaryKey, outbound = _a.outbound, extractKey = _a.extractKey; + var limit = 200; + var modifyChunkSize = _this.db._options.modifyChunkSize; + if (modifyChunkSize) { + if (typeof modifyChunkSize == 'object') { + limit = modifyChunkSize[coreTable.name] || modifyChunkSize['*'] || 200; + } + else { + limit = modifyChunkSize; + } + } + var totalFailures = []; + var successCount = 0; + var failedKeys = []; + var applyMutateResult = function (expectedCount, res) { + var failures = res.failures, numFailures = res.numFailures; + successCount += expectedCount - numFailures; + for (var _i = 0, _a = keys(failures); _i < _a.length; _i++) { + var pos = _a[_i]; + totalFailures.push(failures[pos]); + } + }; + return _this.clone().primaryKeys().then(function (keys) { + var criteria = isPlainKeyRange(ctx) && + ctx.limit === Infinity && + (typeof changes !== 'function' || changes === deleteCallback) && { + index: ctx.index, + range: ctx.range + }; + var nextChunk = function (offset) { + var count = Math.min(limit, keys.length - offset); + return coreTable.getMany({ + trans: trans, + keys: keys.slice(offset, offset + count), + cache: "immutable" + }).then(function (values) { + var addValues = []; + var putValues = []; + var putKeys = outbound ? [] : null; + var deleteKeys = []; + for (var i = 0; i < count; ++i) { + var origValue = values[i]; + var ctx_1 = { + value: deepClone(origValue), + primKey: keys[offset + i] + }; + if (modifyer.call(ctx_1, ctx_1.value, ctx_1) !== false) { + if (ctx_1.value == null) { + deleteKeys.push(keys[offset + i]); + } + else if (!outbound && cmp(extractKey(origValue), extractKey(ctx_1.value)) !== 0) { + deleteKeys.push(keys[offset + i]); + addValues.push(ctx_1.value); + } + else { + putValues.push(ctx_1.value); + if (outbound) + putKeys.push(keys[offset + i]); + } + } + } + return Promise.resolve(addValues.length > 0 && + coreTable.mutate({ trans: trans, type: 'add', values: addValues }) + .then(function (res) { + for (var pos in res.failures) { + deleteKeys.splice(parseInt(pos), 1); + } + applyMutateResult(addValues.length, res); + })).then(function () { return (putValues.length > 0 || (criteria && typeof changes === 'object')) && + coreTable.mutate({ + trans: trans, + type: 'put', + keys: putKeys, + values: putValues, + criteria: criteria, + changeSpec: typeof changes !== 'function' + && changes, + isAdditionalChunk: offset > 0 + }).then(function (res) { return applyMutateResult(putValues.length, res); }); }).then(function () { return (deleteKeys.length > 0 || (criteria && changes === deleteCallback)) && + coreTable.mutate({ + trans: trans, + type: 'delete', + keys: deleteKeys, + criteria: criteria, + isAdditionalChunk: offset > 0 + }).then(function (res) { return applyMutateResult(deleteKeys.length, res); }); }).then(function () { + return keys.length > offset + count && nextChunk(offset + limit); + }); + }); + }; + return nextChunk(0).then(function () { + if (totalFailures.length > 0) + throw new ModifyError("Error modifying one or more objects", totalFailures, successCount, failedKeys); + return keys.length; + }); + }); + }); + }; + Collection.prototype.delete = function () { + var ctx = this._ctx, range = ctx.range; + if (isPlainKeyRange(ctx) && + (ctx.isPrimKey || range.type === 3 )) + { + return this._write(function (trans) { + var primaryKey = ctx.table.core.schema.primaryKey; + var coreRange = range; + return ctx.table.core.count({ trans: trans, query: { index: primaryKey, range: coreRange } }).then(function (count) { + return ctx.table.core.mutate({ trans: trans, type: 'deleteRange', range: coreRange }) + .then(function (_a) { + var failures = _a.failures; _a.lastResult; _a.results; var numFailures = _a.numFailures; + if (numFailures) + throw new ModifyError("Could not delete some values", Object.keys(failures).map(function (pos) { return failures[pos]; }), count - numFailures); + return count - numFailures; + }); + }); + }); + } + return this.modify(deleteCallback); + }; + return Collection; +}()); +var deleteCallback = function (value, ctx) { return ctx.value = null; }; + +function createCollectionConstructor(db) { + return makeClassConstructor(Collection.prototype, function Collection(whereClause, keyRangeGenerator) { + this.db = db; + var keyRange = AnyRange, error = null; + if (keyRangeGenerator) + try { + keyRange = keyRangeGenerator(); + } + catch (ex) { + error = ex; + } + var whereCtx = whereClause._ctx; + var table = whereCtx.table; + var readingHook = table.hook.reading.fire; + this._ctx = { + table: table, + index: whereCtx.index, + isPrimKey: (!whereCtx.index || (table.schema.primKey.keyPath && whereCtx.index === table.schema.primKey.name)), + range: keyRange, + keysOnly: false, + dir: "next", + unique: "", + algorithm: null, + filter: null, + replayFilter: null, + justLimit: true, + isMatch: null, + offset: 0, + limit: Infinity, + error: error, + or: whereCtx.or, + valueMapper: readingHook !== mirror ? readingHook : null + }; + }); +} + +function simpleCompare(a, b) { + return a < b ? -1 : a === b ? 0 : 1; +} +function simpleCompareReverse(a, b) { + return a > b ? -1 : a === b ? 0 : 1; +} + +function fail(collectionOrWhereClause, err, T) { + var collection = collectionOrWhereClause instanceof WhereClause ? + new collectionOrWhereClause.Collection(collectionOrWhereClause) : + collectionOrWhereClause; + collection._ctx.error = T ? new T(err) : new TypeError(err); + return collection; +} +function emptyCollection(whereClause) { + return new whereClause.Collection(whereClause, function () { return rangeEqual(""); }).limit(0); +} +function upperFactory(dir) { + return dir === "next" ? + function (s) { return s.toUpperCase(); } : + function (s) { return s.toLowerCase(); }; +} +function lowerFactory(dir) { + return dir === "next" ? + function (s) { return s.toLowerCase(); } : + function (s) { return s.toUpperCase(); }; +} +function nextCasing(key, lowerKey, upperNeedle, lowerNeedle, cmp, dir) { + var length = Math.min(key.length, lowerNeedle.length); + var llp = -1; + for (var i = 0; i < length; ++i) { + var lwrKeyChar = lowerKey[i]; + if (lwrKeyChar !== lowerNeedle[i]) { + if (cmp(key[i], upperNeedle[i]) < 0) + return key.substr(0, i) + upperNeedle[i] + upperNeedle.substr(i + 1); + if (cmp(key[i], lowerNeedle[i]) < 0) + return key.substr(0, i) + lowerNeedle[i] + upperNeedle.substr(i + 1); + if (llp >= 0) + return key.substr(0, llp) + lowerKey[llp] + upperNeedle.substr(llp + 1); + return null; + } + if (cmp(key[i], lwrKeyChar) < 0) + llp = i; + } + if (length < lowerNeedle.length && dir === "next") + return key + upperNeedle.substr(key.length); + if (length < key.length && dir === "prev") + return key.substr(0, upperNeedle.length); + return (llp < 0 ? null : key.substr(0, llp) + lowerNeedle[llp] + upperNeedle.substr(llp + 1)); +} +function addIgnoreCaseAlgorithm(whereClause, match, needles, suffix) { + var upper, lower, compare, upperNeedles, lowerNeedles, direction, nextKeySuffix, needlesLen = needles.length; + if (!needles.every(function (s) { return typeof s === 'string'; })) { + return fail(whereClause, STRING_EXPECTED); + } + function initDirection(dir) { + upper = upperFactory(dir); + lower = lowerFactory(dir); + compare = (dir === "next" ? simpleCompare : simpleCompareReverse); + var needleBounds = needles.map(function (needle) { + return { lower: lower(needle), upper: upper(needle) }; + }).sort(function (a, b) { + return compare(a.lower, b.lower); + }); + upperNeedles = needleBounds.map(function (nb) { return nb.upper; }); + lowerNeedles = needleBounds.map(function (nb) { return nb.lower; }); + direction = dir; + nextKeySuffix = (dir === "next" ? "" : suffix); + } + initDirection("next"); + var c = new whereClause.Collection(whereClause, function () { return createRange(upperNeedles[0], lowerNeedles[needlesLen - 1] + suffix); }); + c._ondirectionchange = function (direction) { + initDirection(direction); + }; + var firstPossibleNeedle = 0; + c._addAlgorithm(function (cursor, advance, resolve) { + var key = cursor.key; + if (typeof key !== 'string') + return false; + var lowerKey = lower(key); + if (match(lowerKey, lowerNeedles, firstPossibleNeedle)) { + return true; + } + else { + var lowestPossibleCasing = null; + for (var i = firstPossibleNeedle; i < needlesLen; ++i) { + var casing = nextCasing(key, lowerKey, upperNeedles[i], lowerNeedles[i], compare, direction); + if (casing === null && lowestPossibleCasing === null) + firstPossibleNeedle = i + 1; + else if (lowestPossibleCasing === null || compare(lowestPossibleCasing, casing) > 0) { + lowestPossibleCasing = casing; + } + } + if (lowestPossibleCasing !== null) { + advance(function () { cursor.continue(lowestPossibleCasing + nextKeySuffix); }); + } + else { + advance(resolve); + } + return false; + } + }); + return c; +} +function createRange(lower, upper, lowerOpen, upperOpen) { + return { + type: 2 , + lower: lower, + upper: upper, + lowerOpen: lowerOpen, + upperOpen: upperOpen + }; +} +function rangeEqual(value) { + return { + type: 1 , + lower: value, + upper: value + }; +} + +var WhereClause = (function () { + function WhereClause() { + } + Object.defineProperty(WhereClause.prototype, "Collection", { + get: function () { + return this._ctx.table.db.Collection; + }, + enumerable: false, + configurable: true + }); + WhereClause.prototype.between = function (lower, upper, includeLower, includeUpper) { + includeLower = includeLower !== false; + includeUpper = includeUpper === true; + try { + if ((this._cmp(lower, upper) > 0) || + (this._cmp(lower, upper) === 0 && (includeLower || includeUpper) && !(includeLower && includeUpper))) + return emptyCollection(this); + return new this.Collection(this, function () { return createRange(lower, upper, !includeLower, !includeUpper); }); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + }; + WhereClause.prototype.equals = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return rangeEqual(value); }); + }; + WhereClause.prototype.above = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return createRange(value, undefined, true); }); + }; + WhereClause.prototype.aboveOrEqual = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return createRange(value, undefined, false); }); + }; + WhereClause.prototype.below = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return createRange(undefined, value, false, true); }); + }; + WhereClause.prototype.belowOrEqual = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return createRange(undefined, value); }); + }; + WhereClause.prototype.startsWith = function (str) { + if (typeof str !== 'string') + return fail(this, STRING_EXPECTED); + return this.between(str, str + maxString, true, true); + }; + WhereClause.prototype.startsWithIgnoreCase = function (str) { + if (str === "") + return this.startsWith(str); + return addIgnoreCaseAlgorithm(this, function (x, a) { return x.indexOf(a[0]) === 0; }, [str], maxString); + }; + WhereClause.prototype.equalsIgnoreCase = function (str) { + return addIgnoreCaseAlgorithm(this, function (x, a) { return x === a[0]; }, [str], ""); + }; + WhereClause.prototype.anyOfIgnoreCase = function () { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return emptyCollection(this); + return addIgnoreCaseAlgorithm(this, function (x, a) { return a.indexOf(x) !== -1; }, set, ""); + }; + WhereClause.prototype.startsWithAnyOfIgnoreCase = function () { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return emptyCollection(this); + return addIgnoreCaseAlgorithm(this, function (x, a) { return a.some(function (n) { return x.indexOf(n) === 0; }); }, set, maxString); + }; + WhereClause.prototype.anyOf = function () { + var _this = this; + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + var compare = this._cmp; + try { + set.sort(compare); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + if (set.length === 0) + return emptyCollection(this); + var c = new this.Collection(this, function () { return createRange(set[0], set[set.length - 1]); }); + c._ondirectionchange = function (direction) { + compare = (direction === "next" ? + _this._ascending : + _this._descending); + set.sort(compare); + }; + var i = 0; + c._addAlgorithm(function (cursor, advance, resolve) { + var key = cursor.key; + while (compare(key, set[i]) > 0) { + ++i; + if (i === set.length) { + advance(resolve); + return false; + } + } + if (compare(key, set[i]) === 0) { + return true; + } + else { + advance(function () { cursor.continue(set[i]); }); + return false; + } + }); + return c; + }; + WhereClause.prototype.notEqual = function (value) { + return this.inAnyRange([[minKey, value], [value, this.db._maxKey]], { includeLowers: false, includeUppers: false }); + }; + WhereClause.prototype.noneOf = function () { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return new this.Collection(this); + try { + set.sort(this._ascending); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + var ranges = set.reduce(function (res, val) { return res ? + res.concat([[res[res.length - 1][1], val]]) : + [[minKey, val]]; }, null); + ranges.push([set[set.length - 1], this.db._maxKey]); + return this.inAnyRange(ranges, { includeLowers: false, includeUppers: false }); + }; + WhereClause.prototype.inAnyRange = function (ranges, options) { + var _this = this; + var cmp = this._cmp, ascending = this._ascending, descending = this._descending, min = this._min, max = this._max; + if (ranges.length === 0) + return emptyCollection(this); + if (!ranges.every(function (range) { + return range[0] !== undefined && + range[1] !== undefined && + ascending(range[0], range[1]) <= 0; + })) { + return fail(this, "First argument to inAnyRange() must be an Array of two-value Arrays [lower,upper] where upper must not be lower than lower", exceptions.InvalidArgument); + } + var includeLowers = !options || options.includeLowers !== false; + var includeUppers = options && options.includeUppers === true; + function addRange(ranges, newRange) { + var i = 0, l = ranges.length; + for (; i < l; ++i) { + var range = ranges[i]; + if (cmp(newRange[0], range[1]) < 0 && cmp(newRange[1], range[0]) > 0) { + range[0] = min(range[0], newRange[0]); + range[1] = max(range[1], newRange[1]); + break; + } + } + if (i === l) + ranges.push(newRange); + return ranges; + } + var sortDirection = ascending; + function rangeSorter(a, b) { return sortDirection(a[0], b[0]); } + var set; + try { + set = ranges.reduce(addRange, []); + set.sort(rangeSorter); + } + catch (ex) { + return fail(this, INVALID_KEY_ARGUMENT); + } + var rangePos = 0; + var keyIsBeyondCurrentEntry = includeUppers ? + function (key) { return ascending(key, set[rangePos][1]) > 0; } : + function (key) { return ascending(key, set[rangePos][1]) >= 0; }; + var keyIsBeforeCurrentEntry = includeLowers ? + function (key) { return descending(key, set[rangePos][0]) > 0; } : + function (key) { return descending(key, set[rangePos][0]) >= 0; }; + function keyWithinCurrentRange(key) { + return !keyIsBeyondCurrentEntry(key) && !keyIsBeforeCurrentEntry(key); + } + var checkKey = keyIsBeyondCurrentEntry; + var c = new this.Collection(this, function () { return createRange(set[0][0], set[set.length - 1][1], !includeLowers, !includeUppers); }); + c._ondirectionchange = function (direction) { + if (direction === "next") { + checkKey = keyIsBeyondCurrentEntry; + sortDirection = ascending; + } + else { + checkKey = keyIsBeforeCurrentEntry; + sortDirection = descending; + } + set.sort(rangeSorter); + }; + c._addAlgorithm(function (cursor, advance, resolve) { + var key = cursor.key; + while (checkKey(key)) { + ++rangePos; + if (rangePos === set.length) { + advance(resolve); + return false; + } + } + if (keyWithinCurrentRange(key)) { + return true; + } + else if (_this._cmp(key, set[rangePos][1]) === 0 || _this._cmp(key, set[rangePos][0]) === 0) { + return false; + } + else { + advance(function () { + if (sortDirection === ascending) + cursor.continue(set[rangePos][0]); + else + cursor.continue(set[rangePos][1]); + }); + return false; + } + }); + return c; + }; + WhereClause.prototype.startsWithAnyOf = function () { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (!set.every(function (s) { return typeof s === 'string'; })) { + return fail(this, "startsWithAnyOf() only works with strings"); + } + if (set.length === 0) + return emptyCollection(this); + return this.inAnyRange(set.map(function (str) { return [str, str + maxString]; })); + }; + return WhereClause; +}()); + +function createWhereClauseConstructor(db) { + return makeClassConstructor(WhereClause.prototype, function WhereClause(table, index, orCollection) { + this.db = db; + this._ctx = { + table: table, + index: index === ":id" ? null : index, + or: orCollection + }; + this._cmp = this._ascending = cmp; + this._descending = function (a, b) { return cmp(b, a); }; + this._max = function (a, b) { return cmp(a, b) > 0 ? a : b; }; + this._min = function (a, b) { return cmp(a, b) < 0 ? a : b; }; + this._IDBKeyRange = db._deps.IDBKeyRange; + if (!this._IDBKeyRange) + throw new exceptions.MissingAPI(); + }); +} + +function eventRejectHandler(reject) { + return wrap(function (event) { + preventDefault(event); + reject(event.target.error); + return false; + }); +} +function preventDefault(event) { + if (event.stopPropagation) + event.stopPropagation(); + if (event.preventDefault) + event.preventDefault(); +} + +var DEXIE_STORAGE_MUTATED_EVENT_NAME = 'storagemutated'; +var STORAGE_MUTATED_DOM_EVENT_NAME = 'x-storagemutated-1'; +var globalEvents = Events(null, DEXIE_STORAGE_MUTATED_EVENT_NAME); + +var Transaction = (function () { + function Transaction() { + } + Transaction.prototype._lock = function () { + assert(!PSD.global); + ++this._reculock; + if (this._reculock === 1 && !PSD.global) + PSD.lockOwnerFor = this; + return this; + }; + Transaction.prototype._unlock = function () { + assert(!PSD.global); + if (--this._reculock === 0) { + if (!PSD.global) + PSD.lockOwnerFor = null; + while (this._blockedFuncs.length > 0 && !this._locked()) { + var fnAndPSD = this._blockedFuncs.shift(); + try { + usePSD(fnAndPSD[1], fnAndPSD[0]); + } + catch (e) { } + } + } + return this; + }; + Transaction.prototype._locked = function () { + return this._reculock && PSD.lockOwnerFor !== this; + }; + Transaction.prototype.create = function (idbtrans) { + var _this = this; + if (!this.mode) + return this; + var idbdb = this.db.idbdb; + var dbOpenError = this.db._state.dbOpenError; + assert(!this.idbtrans); + if (!idbtrans && !idbdb) { + switch (dbOpenError && dbOpenError.name) { + case "DatabaseClosedError": + throw new exceptions.DatabaseClosed(dbOpenError); + case "MissingAPIError": + throw new exceptions.MissingAPI(dbOpenError.message, dbOpenError); + default: + throw new exceptions.OpenFailed(dbOpenError); + } + } + if (!this.active) + throw new exceptions.TransactionInactive(); + assert(this._completion._state === null); + idbtrans = this.idbtrans = idbtrans || + (this.db.core + ? this.db.core.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability }) + : idbdb.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability })); + idbtrans.onerror = wrap(function (ev) { + preventDefault(ev); + _this._reject(idbtrans.error); + }); + idbtrans.onabort = wrap(function (ev) { + preventDefault(ev); + _this.active && _this._reject(new exceptions.Abort(idbtrans.error)); + _this.active = false; + _this.on("abort").fire(ev); + }); + idbtrans.oncomplete = wrap(function () { + _this.active = false; + _this._resolve(); + if ('mutatedParts' in idbtrans) { + globalEvents.storagemutated.fire(idbtrans["mutatedParts"]); + } + }); + return this; + }; + Transaction.prototype._promise = function (mode, fn, bWriteLock) { + var _this = this; + if (mode === 'readwrite' && this.mode !== 'readwrite') + return rejection(new exceptions.ReadOnly("Transaction is readonly")); + if (!this.active) + return rejection(new exceptions.TransactionInactive()); + if (this._locked()) { + return new DexiePromise(function (resolve, reject) { + _this._blockedFuncs.push([function () { + _this._promise(mode, fn, bWriteLock).then(resolve, reject); + }, PSD]); + }); + } + else if (bWriteLock) { + return newScope(function () { + var p = new DexiePromise(function (resolve, reject) { + _this._lock(); + var rv = fn(resolve, reject, _this); + if (rv && rv.then) + rv.then(resolve, reject); + }); + p.finally(function () { return _this._unlock(); }); + p._lib = true; + return p; + }); + } + else { + var p = new DexiePromise(function (resolve, reject) { + var rv = fn(resolve, reject, _this); + if (rv && rv.then) + rv.then(resolve, reject); + }); + p._lib = true; + return p; + } + }; + Transaction.prototype._root = function () { + return this.parent ? this.parent._root() : this; + }; + Transaction.prototype.waitFor = function (promiseLike) { + var root = this._root(); + var promise = DexiePromise.resolve(promiseLike); + if (root._waitingFor) { + root._waitingFor = root._waitingFor.then(function () { return promise; }); + } + else { + root._waitingFor = promise; + root._waitingQueue = []; + var store = root.idbtrans.objectStore(root.storeNames[0]); + (function spin() { + ++root._spinCount; + while (root._waitingQueue.length) + (root._waitingQueue.shift())(); + if (root._waitingFor) + store.get(-Infinity).onsuccess = spin; + }()); + } + var currentWaitPromise = root._waitingFor; + return new DexiePromise(function (resolve, reject) { + promise.then(function (res) { return root._waitingQueue.push(wrap(resolve.bind(null, res))); }, function (err) { return root._waitingQueue.push(wrap(reject.bind(null, err))); }).finally(function () { + if (root._waitingFor === currentWaitPromise) { + root._waitingFor = null; + } + }); + }); + }; + Transaction.prototype.abort = function () { + if (this.active) { + this.active = false; + if (this.idbtrans) + this.idbtrans.abort(); + this._reject(new exceptions.Abort()); + } + }; + Transaction.prototype.table = function (tableName) { + var memoizedTables = (this._memoizedTables || (this._memoizedTables = {})); + if (hasOwn(memoizedTables, tableName)) + return memoizedTables[tableName]; + var tableSchema = this.schema[tableName]; + if (!tableSchema) { + throw new exceptions.NotFound("Table " + tableName + " not part of transaction"); + } + var transactionBoundTable = new this.db.Table(tableName, tableSchema, this); + transactionBoundTable.core = this.db.core.table(tableName); + memoizedTables[tableName] = transactionBoundTable; + return transactionBoundTable; + }; + return Transaction; +}()); + +function createTransactionConstructor(db) { + return makeClassConstructor(Transaction.prototype, function Transaction(mode, storeNames, dbschema, chromeTransactionDurability, parent) { + var _this = this; + this.db = db; + this.mode = mode; + this.storeNames = storeNames; + this.schema = dbschema; + this.chromeTransactionDurability = chromeTransactionDurability; + this.idbtrans = null; + this.on = Events(this, "complete", "error", "abort"); + this.parent = parent || null; + this.active = true; + this._reculock = 0; + this._blockedFuncs = []; + this._resolve = null; + this._reject = null; + this._waitingFor = null; + this._waitingQueue = null; + this._spinCount = 0; + this._completion = new DexiePromise(function (resolve, reject) { + _this._resolve = resolve; + _this._reject = reject; + }); + this._completion.then(function () { + _this.active = false; + _this.on.complete.fire(); + }, function (e) { + var wasActive = _this.active; + _this.active = false; + _this.on.error.fire(e); + _this.parent ? + _this.parent._reject(e) : + wasActive && _this.idbtrans && _this.idbtrans.abort(); + return rejection(e); + }); + }); +} + +function createIndexSpec(name, keyPath, unique, multi, auto, compound, isPrimKey) { + return { + name: name, + keyPath: keyPath, + unique: unique, + multi: multi, + auto: auto, + compound: compound, + src: (unique && !isPrimKey ? '&' : '') + (multi ? '*' : '') + (auto ? "++" : "") + nameFromKeyPath(keyPath) + }; +} +function nameFromKeyPath(keyPath) { + return typeof keyPath === 'string' ? + keyPath : + keyPath ? ('[' + [].join.call(keyPath, '+') + ']') : ""; +} + +function createTableSchema(name, primKey, indexes) { + return { + name: name, + primKey: primKey, + indexes: indexes, + mappedClass: null, + idxByName: arrayToObject(indexes, function (index) { return [index.name, index]; }) + }; +} + +function safariMultiStoreFix(storeNames) { + return storeNames.length === 1 ? storeNames[0] : storeNames; +} +var getMaxKey = function (IdbKeyRange) { + try { + IdbKeyRange.only([[]]); + getMaxKey = function () { return [[]]; }; + return [[]]; + } + catch (e) { + getMaxKey = function () { return maxString; }; + return maxString; + } +}; + +function getKeyExtractor(keyPath) { + if (keyPath == null) { + return function () { return undefined; }; + } + else if (typeof keyPath === 'string') { + return getSinglePathKeyExtractor(keyPath); + } + else { + return function (obj) { return getByKeyPath(obj, keyPath); }; + } +} +function getSinglePathKeyExtractor(keyPath) { + var split = keyPath.split('.'); + if (split.length === 1) { + return function (obj) { return obj[keyPath]; }; + } + else { + return function (obj) { return getByKeyPath(obj, keyPath); }; + } +} + +function arrayify(arrayLike) { + return [].slice.call(arrayLike); +} +var _id_counter = 0; +function getKeyPathAlias(keyPath) { + return keyPath == null ? + ":id" : + typeof keyPath === 'string' ? + keyPath : + "[".concat(keyPath.join('+'), "]"); +} +function createDBCore(db, IdbKeyRange, tmpTrans) { + function extractSchema(db, trans) { + var tables = arrayify(db.objectStoreNames); + return { + schema: { + name: db.name, + tables: tables.map(function (table) { return trans.objectStore(table); }).map(function (store) { + var keyPath = store.keyPath, autoIncrement = store.autoIncrement; + var compound = isArray(keyPath); + var outbound = keyPath == null; + var indexByKeyPath = {}; + var result = { + name: store.name, + primaryKey: { + name: null, + isPrimaryKey: true, + outbound: outbound, + compound: compound, + keyPath: keyPath, + autoIncrement: autoIncrement, + unique: true, + extractKey: getKeyExtractor(keyPath) + }, + indexes: arrayify(store.indexNames).map(function (indexName) { return store.index(indexName); }) + .map(function (index) { + var name = index.name, unique = index.unique, multiEntry = index.multiEntry, keyPath = index.keyPath; + var compound = isArray(keyPath); + var result = { + name: name, + compound: compound, + keyPath: keyPath, + unique: unique, + multiEntry: multiEntry, + extractKey: getKeyExtractor(keyPath) + }; + indexByKeyPath[getKeyPathAlias(keyPath)] = result; + return result; + }), + getIndexByKeyPath: function (keyPath) { return indexByKeyPath[getKeyPathAlias(keyPath)]; } + }; + indexByKeyPath[":id"] = result.primaryKey; + if (keyPath != null) { + indexByKeyPath[getKeyPathAlias(keyPath)] = result.primaryKey; + } + return result; + }) + }, + hasGetAll: tables.length > 0 && ('getAll' in trans.objectStore(tables[0])) && + !(typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && + !/(Chrome\/|Edge\/)/.test(navigator.userAgent) && + [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) + }; + } + function makeIDBKeyRange(range) { + if (range.type === 3 ) + return null; + if (range.type === 4 ) + throw new Error("Cannot convert never type to IDBKeyRange"); + var lower = range.lower, upper = range.upper, lowerOpen = range.lowerOpen, upperOpen = range.upperOpen; + var idbRange = lower === undefined ? + upper === undefined ? + null : + IdbKeyRange.upperBound(upper, !!upperOpen) : + upper === undefined ? + IdbKeyRange.lowerBound(lower, !!lowerOpen) : + IdbKeyRange.bound(lower, upper, !!lowerOpen, !!upperOpen); + return idbRange; + } + function createDbCoreTable(tableSchema) { + var tableName = tableSchema.name; + function mutate(_a) { + var trans = _a.trans, type = _a.type, keys = _a.keys, values = _a.values, range = _a.range; + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var store = trans.objectStore(tableName); + var outbound = store.keyPath == null; + var isAddOrPut = type === "put" || type === "add"; + if (!isAddOrPut && type !== 'delete' && type !== 'deleteRange') + throw new Error("Invalid operation type: " + type); + var length = (keys || values || { length: 1 }).length; + if (keys && values && keys.length !== values.length) { + throw new Error("Given keys array must have same length as given values array."); + } + if (length === 0) + return resolve({ numFailures: 0, failures: {}, results: [], lastResult: undefined }); + var req; + var reqs = []; + var failures = []; + var numFailures = 0; + var errorHandler = function (event) { + ++numFailures; + preventDefault(event); + }; + if (type === 'deleteRange') { + if (range.type === 4 ) + return resolve({ numFailures: numFailures, failures: failures, results: [], lastResult: undefined }); + if (range.type === 3 ) + reqs.push(req = store.clear()); + else + reqs.push(req = store.delete(makeIDBKeyRange(range))); + } + else { + var _a = isAddOrPut ? + outbound ? + [values, keys] : + [values, null] : + [keys, null], args1 = _a[0], args2 = _a[1]; + if (isAddOrPut) { + for (var i = 0; i < length; ++i) { + reqs.push(req = (args2 && args2[i] !== undefined ? + store[type](args1[i], args2[i]) : + store[type](args1[i]))); + req.onerror = errorHandler; + } + } + else { + for (var i = 0; i < length; ++i) { + reqs.push(req = store[type](args1[i])); + req.onerror = errorHandler; + } + } + } + var done = function (event) { + var lastResult = event.target.result; + reqs.forEach(function (req, i) { return req.error != null && (failures[i] = req.error); }); + resolve({ + numFailures: numFailures, + failures: failures, + results: type === "delete" ? keys : reqs.map(function (req) { return req.result; }), + lastResult: lastResult + }); + }; + req.onerror = function (event) { + errorHandler(event); + done(event); + }; + req.onsuccess = done; + }); + } + function openCursor(_a) { + var trans = _a.trans, values = _a.values, query = _a.query, reverse = _a.reverse, unique = _a.unique; + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var index = query.index, range = query.range; + var store = trans.objectStore(tableName); + var source = index.isPrimaryKey ? + store : + store.index(index.name); + var direction = reverse ? + unique ? + "prevunique" : + "prev" : + unique ? + "nextunique" : + "next"; + var req = values || !('openKeyCursor' in source) ? + source.openCursor(makeIDBKeyRange(range), direction) : + source.openKeyCursor(makeIDBKeyRange(range), direction); + req.onerror = eventRejectHandler(reject); + req.onsuccess = wrap(function (ev) { + var cursor = req.result; + if (!cursor) { + resolve(null); + return; + } + cursor.___id = ++_id_counter; + cursor.done = false; + var _cursorContinue = cursor.continue.bind(cursor); + var _cursorContinuePrimaryKey = cursor.continuePrimaryKey; + if (_cursorContinuePrimaryKey) + _cursorContinuePrimaryKey = _cursorContinuePrimaryKey.bind(cursor); + var _cursorAdvance = cursor.advance.bind(cursor); + var doThrowCursorIsNotStarted = function () { throw new Error("Cursor not started"); }; + var doThrowCursorIsStopped = function () { throw new Error("Cursor not stopped"); }; + cursor.trans = trans; + cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsNotStarted; + cursor.fail = wrap(reject); + cursor.next = function () { + var _this = this; + var gotOne = 1; + return this.start(function () { return gotOne-- ? _this.continue() : _this.stop(); }).then(function () { return _this; }); + }; + cursor.start = function (callback) { + var iterationPromise = new Promise(function (resolveIteration, rejectIteration) { + resolveIteration = wrap(resolveIteration); + req.onerror = eventRejectHandler(rejectIteration); + cursor.fail = rejectIteration; + cursor.stop = function (value) { + cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsStopped; + resolveIteration(value); + }; + }); + var guardedCallback = function () { + if (req.result) { + try { + callback(); + } + catch (err) { + cursor.fail(err); + } + } + else { + cursor.done = true; + cursor.start = function () { throw new Error("Cursor behind last entry"); }; + cursor.stop(); + } + }; + req.onsuccess = wrap(function (ev) { + req.onsuccess = guardedCallback; + guardedCallback(); + }); + cursor.continue = _cursorContinue; + cursor.continuePrimaryKey = _cursorContinuePrimaryKey; + cursor.advance = _cursorAdvance; + guardedCallback(); + return iterationPromise; + }; + resolve(cursor); + }, reject); + }); + } + function query(hasGetAll) { + return function (request) { + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var trans = request.trans, values = request.values, limit = request.limit, query = request.query; + var nonInfinitLimit = limit === Infinity ? undefined : limit; + var index = query.index, range = query.range; + var store = trans.objectStore(tableName); + var source = index.isPrimaryKey ? store : store.index(index.name); + var idbKeyRange = makeIDBKeyRange(range); + if (limit === 0) + return resolve({ result: [] }); + if (hasGetAll) { + var req = values ? + source.getAll(idbKeyRange, nonInfinitLimit) : + source.getAllKeys(idbKeyRange, nonInfinitLimit); + req.onsuccess = function (event) { return resolve({ result: event.target.result }); }; + req.onerror = eventRejectHandler(reject); + } + else { + var count_1 = 0; + var req_1 = values || !('openKeyCursor' in source) ? + source.openCursor(idbKeyRange) : + source.openKeyCursor(idbKeyRange); + var result_1 = []; + req_1.onsuccess = function (event) { + var cursor = req_1.result; + if (!cursor) + return resolve({ result: result_1 }); + result_1.push(values ? cursor.value : cursor.primaryKey); + if (++count_1 === limit) + return resolve({ result: result_1 }); + cursor.continue(); + }; + req_1.onerror = eventRejectHandler(reject); + } + }); + }; + } + return { + name: tableName, + schema: tableSchema, + mutate: mutate, + getMany: function (_a) { + var trans = _a.trans, keys = _a.keys; + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var store = trans.objectStore(tableName); + var length = keys.length; + var result = new Array(length); + var keyCount = 0; + var callbackCount = 0; + var req; + var successHandler = function (event) { + var req = event.target; + if ((result[req._pos] = req.result) != null) + ; + if (++callbackCount === keyCount) + resolve(result); + }; + var errorHandler = eventRejectHandler(reject); + for (var i = 0; i < length; ++i) { + var key = keys[i]; + if (key != null) { + req = store.get(keys[i]); + req._pos = i; + req.onsuccess = successHandler; + req.onerror = errorHandler; + ++keyCount; + } + } + if (keyCount === 0) + resolve(result); + }); + }, + get: function (_a) { + var trans = _a.trans, key = _a.key; + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var store = trans.objectStore(tableName); + var req = store.get(key); + req.onsuccess = function (event) { return resolve(event.target.result); }; + req.onerror = eventRejectHandler(reject); + }); + }, + query: query(hasGetAll), + openCursor: openCursor, + count: function (_a) { + var query = _a.query, trans = _a.trans; + var index = query.index, range = query.range; + return new Promise(function (resolve, reject) { + var store = trans.objectStore(tableName); + var source = index.isPrimaryKey ? store : store.index(index.name); + var idbKeyRange = makeIDBKeyRange(range); + var req = idbKeyRange ? source.count(idbKeyRange) : source.count(); + req.onsuccess = wrap(function (ev) { return resolve(ev.target.result); }); + req.onerror = eventRejectHandler(reject); + }); + } + }; + } + var _a = extractSchema(db, tmpTrans), schema = _a.schema, hasGetAll = _a.hasGetAll; + var tables = schema.tables.map(function (tableSchema) { return createDbCoreTable(tableSchema); }); + var tableMap = {}; + tables.forEach(function (table) { return tableMap[table.name] = table; }); + return { + stack: "dbcore", + transaction: db.transaction.bind(db), + table: function (name) { + var result = tableMap[name]; + if (!result) + throw new Error("Table '".concat(name, "' not found")); + return tableMap[name]; + }, + MIN_KEY: -Infinity, + MAX_KEY: getMaxKey(IdbKeyRange), + schema: schema + }; +} + +function createMiddlewareStack(stackImpl, middlewares) { + return middlewares.reduce(function (down, _a) { + var create = _a.create; + return (__assign(__assign({}, down), create(down))); + }, stackImpl); +} +function createMiddlewareStacks(middlewares, idbdb, _a, tmpTrans) { + var IDBKeyRange = _a.IDBKeyRange; _a.indexedDB; + var dbcore = createMiddlewareStack(createDBCore(idbdb, IDBKeyRange, tmpTrans), middlewares.dbcore); + return { + dbcore: dbcore + }; +} +function generateMiddlewareStacks(db, tmpTrans) { + var idbdb = tmpTrans.db; + var stacks = createMiddlewareStacks(db._middlewares, idbdb, db._deps, tmpTrans); + db.core = stacks.dbcore; + db.tables.forEach(function (table) { + var tableName = table.name; + if (db.core.schema.tables.some(function (tbl) { return tbl.name === tableName; })) { + table.core = db.core.table(tableName); + if (db[tableName] instanceof db.Table) { + db[tableName].core = table.core; + } + } + }); +} + +function setApiOnPlace(db, objs, tableNames, dbschema) { + tableNames.forEach(function (tableName) { + var schema = dbschema[tableName]; + objs.forEach(function (obj) { + var propDesc = getPropertyDescriptor(obj, tableName); + if (!propDesc || ("value" in propDesc && propDesc.value === undefined)) { + if (obj === db.Transaction.prototype || obj instanceof db.Transaction) { + setProp(obj, tableName, { + get: function () { return this.table(tableName); }, + set: function (value) { + defineProperty(this, tableName, { value: value, writable: true, configurable: true, enumerable: true }); + } + }); + } + else { + obj[tableName] = new db.Table(tableName, schema); + } + } + }); + }); +} +function removeTablesApi(db, objs) { + objs.forEach(function (obj) { + for (var key in obj) { + if (obj[key] instanceof db.Table) + delete obj[key]; + } + }); +} +function lowerVersionFirst(a, b) { + return a._cfg.version - b._cfg.version; +} +function runUpgraders(db, oldVersion, idbUpgradeTrans, reject) { + var globalSchema = db._dbSchema; + if (idbUpgradeTrans.objectStoreNames.contains('$meta') && !globalSchema.$meta) { + globalSchema.$meta = createTableSchema("$meta", parseIndexSyntax("")[0], []); + db._storeNames.push('$meta'); + } + var trans = db._createTransaction('readwrite', db._storeNames, globalSchema); + trans.create(idbUpgradeTrans); + trans._completion.catch(reject); + var rejectTransaction = trans._reject.bind(trans); + var transless = PSD.transless || PSD; + newScope(function () { + PSD.trans = trans; + PSD.transless = transless; + if (oldVersion === 0) { + keys(globalSchema).forEach(function (tableName) { + createTable(idbUpgradeTrans, tableName, globalSchema[tableName].primKey, globalSchema[tableName].indexes); + }); + generateMiddlewareStacks(db, idbUpgradeTrans); + DexiePromise.follow(function () { return db.on.populate.fire(trans); }).catch(rejectTransaction); + } + else { + generateMiddlewareStacks(db, idbUpgradeTrans); + return getExistingVersion(db, trans, oldVersion) + .then(function (oldVersion) { return updateTablesAndIndexes(db, oldVersion, trans, idbUpgradeTrans); }) + .catch(rejectTransaction); + } + }); +} +function patchCurrentVersion(db, idbUpgradeTrans) { + createMissingTables(db._dbSchema, idbUpgradeTrans); + if (idbUpgradeTrans.db.version % 10 === 0 && !idbUpgradeTrans.objectStoreNames.contains('$meta')) { + idbUpgradeTrans.db.createObjectStore('$meta').add(Math.ceil((idbUpgradeTrans.db.version / 10) - 1), 'version'); + } + var globalSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans); + adjustToExistingIndexNames(db, db._dbSchema, idbUpgradeTrans); + var diff = getSchemaDiff(globalSchema, db._dbSchema); + var _loop_1 = function (tableChange) { + if (tableChange.change.length || tableChange.recreate) { + console.warn("Unable to patch indexes of table ".concat(tableChange.name, " because it has changes on the type of index or primary key.")); + return { value: void 0 }; + } + var store = idbUpgradeTrans.objectStore(tableChange.name); + tableChange.add.forEach(function (idx) { + if (debug) + console.debug("Dexie upgrade patch: Creating missing index ".concat(tableChange.name, ".").concat(idx.src)); + addIndex(store, idx); + }); + }; + for (var _i = 0, _a = diff.change; _i < _a.length; _i++) { + var tableChange = _a[_i]; + var state_1 = _loop_1(tableChange); + if (typeof state_1 === "object") + return state_1.value; + } +} +function getExistingVersion(db, trans, oldVersion) { + if (trans.storeNames.includes('$meta')) { + return trans.table('$meta').get('version').then(function (metaVersion) { + return metaVersion != null ? metaVersion : oldVersion; + }); + } + else { + return DexiePromise.resolve(oldVersion); + } +} +function updateTablesAndIndexes(db, oldVersion, trans, idbUpgradeTrans) { + var queue = []; + var versions = db._versions; + var globalSchema = db._dbSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans); + var versToRun = versions.filter(function (v) { return v._cfg.version >= oldVersion; }); + if (versToRun.length === 0) { + return DexiePromise.resolve(); + } + versToRun.forEach(function (version) { + queue.push(function () { + var oldSchema = globalSchema; + var newSchema = version._cfg.dbschema; + adjustToExistingIndexNames(db, oldSchema, idbUpgradeTrans); + adjustToExistingIndexNames(db, newSchema, idbUpgradeTrans); + globalSchema = db._dbSchema = newSchema; + var diff = getSchemaDiff(oldSchema, newSchema); + diff.add.forEach(function (tuple) { + createTable(idbUpgradeTrans, tuple[0], tuple[1].primKey, tuple[1].indexes); + }); + diff.change.forEach(function (change) { + if (change.recreate) { + throw new exceptions.Upgrade("Not yet support for changing primary key"); + } + else { + var store_1 = idbUpgradeTrans.objectStore(change.name); + change.add.forEach(function (idx) { return addIndex(store_1, idx); }); + change.change.forEach(function (idx) { + store_1.deleteIndex(idx.name); + addIndex(store_1, idx); + }); + change.del.forEach(function (idxName) { return store_1.deleteIndex(idxName); }); + } + }); + var contentUpgrade = version._cfg.contentUpgrade; + if (contentUpgrade && version._cfg.version > oldVersion) { + generateMiddlewareStacks(db, idbUpgradeTrans); + trans._memoizedTables = {}; + var upgradeSchema_1 = shallowClone(newSchema); + diff.del.forEach(function (table) { + upgradeSchema_1[table] = oldSchema[table]; + }); + removeTablesApi(db, [db.Transaction.prototype]); + setApiOnPlace(db, [db.Transaction.prototype], keys(upgradeSchema_1), upgradeSchema_1); + trans.schema = upgradeSchema_1; + var contentUpgradeIsAsync_1 = isAsyncFunction(contentUpgrade); + if (contentUpgradeIsAsync_1) { + incrementExpectedAwaits(); + } + var returnValue_1; + var promiseFollowed = DexiePromise.follow(function () { + returnValue_1 = contentUpgrade(trans); + if (returnValue_1) { + if (contentUpgradeIsAsync_1) { + var decrementor = decrementExpectedAwaits.bind(null, null); + returnValue_1.then(decrementor, decrementor); + } + } + }); + return (returnValue_1 && typeof returnValue_1.then === 'function' ? + DexiePromise.resolve(returnValue_1) : promiseFollowed.then(function () { return returnValue_1; })); + } + }); + queue.push(function (idbtrans) { + var newSchema = version._cfg.dbschema; + deleteRemovedTables(newSchema, idbtrans); + removeTablesApi(db, [db.Transaction.prototype]); + setApiOnPlace(db, [db.Transaction.prototype], db._storeNames, db._dbSchema); + trans.schema = db._dbSchema; + }); + queue.push(function (idbtrans) { + if (db.idbdb.objectStoreNames.contains('$meta')) { + if (Math.ceil(db.idbdb.version / 10) === version._cfg.version) { + db.idbdb.deleteObjectStore('$meta'); + delete db._dbSchema.$meta; + db._storeNames = db._storeNames.filter(function (name) { return name !== '$meta'; }); + } + else { + idbtrans.objectStore('$meta').put(version._cfg.version, 'version'); + } + } + }); + }); + function runQueue() { + return queue.length ? DexiePromise.resolve(queue.shift()(trans.idbtrans)).then(runQueue) : + DexiePromise.resolve(); + } + return runQueue().then(function () { + createMissingTables(globalSchema, idbUpgradeTrans); + }); +} +function getSchemaDiff(oldSchema, newSchema) { + var diff = { + del: [], + add: [], + change: [] + }; + var table; + for (table in oldSchema) { + if (!newSchema[table]) + diff.del.push(table); + } + for (table in newSchema) { + var oldDef = oldSchema[table], newDef = newSchema[table]; + if (!oldDef) { + diff.add.push([table, newDef]); + } + else { + var change = { + name: table, + def: newDef, + recreate: false, + del: [], + add: [], + change: [] + }; + if (( + '' + (oldDef.primKey.keyPath || '')) !== ('' + (newDef.primKey.keyPath || '')) || + (oldDef.primKey.auto !== newDef.primKey.auto)) { + change.recreate = true; + diff.change.push(change); + } + else { + var oldIndexes = oldDef.idxByName; + var newIndexes = newDef.idxByName; + var idxName = void 0; + for (idxName in oldIndexes) { + if (!newIndexes[idxName]) + change.del.push(idxName); + } + for (idxName in newIndexes) { + var oldIdx = oldIndexes[idxName], newIdx = newIndexes[idxName]; + if (!oldIdx) + change.add.push(newIdx); + else if (oldIdx.src !== newIdx.src) + change.change.push(newIdx); + } + if (change.del.length > 0 || change.add.length > 0 || change.change.length > 0) { + diff.change.push(change); + } + } + } + } + return diff; +} +function createTable(idbtrans, tableName, primKey, indexes) { + var store = idbtrans.db.createObjectStore(tableName, primKey.keyPath ? + { keyPath: primKey.keyPath, autoIncrement: primKey.auto } : + { autoIncrement: primKey.auto }); + indexes.forEach(function (idx) { return addIndex(store, idx); }); + return store; +} +function createMissingTables(newSchema, idbtrans) { + keys(newSchema).forEach(function (tableName) { + if (!idbtrans.db.objectStoreNames.contains(tableName)) { + if (debug) + console.debug('Dexie: Creating missing table', tableName); + createTable(idbtrans, tableName, newSchema[tableName].primKey, newSchema[tableName].indexes); + } + }); +} +function deleteRemovedTables(newSchema, idbtrans) { + [].slice.call(idbtrans.db.objectStoreNames).forEach(function (storeName) { + return newSchema[storeName] == null && idbtrans.db.deleteObjectStore(storeName); + }); +} +function addIndex(store, idx) { + store.createIndex(idx.name, idx.keyPath, { unique: idx.unique, multiEntry: idx.multi }); +} +function buildGlobalSchema(db, idbdb, tmpTrans) { + var globalSchema = {}; + var dbStoreNames = slice(idbdb.objectStoreNames, 0); + dbStoreNames.forEach(function (storeName) { + var store = tmpTrans.objectStore(storeName); + var keyPath = store.keyPath; + var primKey = createIndexSpec(nameFromKeyPath(keyPath), keyPath || "", true, false, !!store.autoIncrement, keyPath && typeof keyPath !== "string", true); + var indexes = []; + for (var j = 0; j < store.indexNames.length; ++j) { + var idbindex = store.index(store.indexNames[j]); + keyPath = idbindex.keyPath; + var index = createIndexSpec(idbindex.name, keyPath, !!idbindex.unique, !!idbindex.multiEntry, false, keyPath && typeof keyPath !== "string", false); + indexes.push(index); + } + globalSchema[storeName] = createTableSchema(storeName, primKey, indexes); + }); + return globalSchema; +} +function readGlobalSchema(db, idbdb, tmpTrans) { + db.verno = idbdb.version / 10; + var globalSchema = db._dbSchema = buildGlobalSchema(db, idbdb, tmpTrans); + db._storeNames = slice(idbdb.objectStoreNames, 0); + setApiOnPlace(db, [db._allTables], keys(globalSchema), globalSchema); +} +function verifyInstalledSchema(db, tmpTrans) { + var installedSchema = buildGlobalSchema(db, db.idbdb, tmpTrans); + var diff = getSchemaDiff(installedSchema, db._dbSchema); + return !(diff.add.length || diff.change.some(function (ch) { return ch.add.length || ch.change.length; })); +} +function adjustToExistingIndexNames(db, schema, idbtrans) { + var storeNames = idbtrans.db.objectStoreNames; + for (var i = 0; i < storeNames.length; ++i) { + var storeName = storeNames[i]; + var store = idbtrans.objectStore(storeName); + db._hasGetAll = 'getAll' in store; + for (var j = 0; j < store.indexNames.length; ++j) { + var indexName = store.indexNames[j]; + var keyPath = store.index(indexName).keyPath; + var dexieName = typeof keyPath === 'string' ? keyPath : "[" + slice(keyPath).join('+') + "]"; + if (schema[storeName]) { + var indexSpec = schema[storeName].idxByName[dexieName]; + if (indexSpec) { + indexSpec.name = indexName; + delete schema[storeName].idxByName[dexieName]; + schema[storeName].idxByName[indexName] = indexSpec; + } + } + } + } + if (typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && + !/(Chrome\/|Edge\/)/.test(navigator.userAgent) && + _global.WorkerGlobalScope && _global instanceof _global.WorkerGlobalScope && + [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) { + db._hasGetAll = false; + } +} +function parseIndexSyntax(primKeyAndIndexes) { + return primKeyAndIndexes.split(',').map(function (index, indexNum) { + index = index.trim(); + var name = index.replace(/([&*]|\+\+)/g, ""); + var keyPath = /^\[/.test(name) ? name.match(/^\[(.*)\]$/)[1].split('+') : name; + return createIndexSpec(name, keyPath || null, /\&/.test(index), /\*/.test(index), /\+\+/.test(index), isArray(keyPath), indexNum === 0); + }); +} + +var Version = (function () { + function Version() { + } + Version.prototype._parseStoresSpec = function (stores, outSchema) { + keys(stores).forEach(function (tableName) { + if (stores[tableName] !== null) { + var indexes = parseIndexSyntax(stores[tableName]); + var primKey = indexes.shift(); + primKey.unique = true; + if (primKey.multi) + throw new exceptions.Schema("Primary key cannot be multi-valued"); + indexes.forEach(function (idx) { + if (idx.auto) + throw new exceptions.Schema("Only primary key can be marked as autoIncrement (++)"); + if (!idx.keyPath) + throw new exceptions.Schema("Index must have a name and cannot be an empty string"); + }); + outSchema[tableName] = createTableSchema(tableName, primKey, indexes); + } + }); + }; + Version.prototype.stores = function (stores) { + var db = this.db; + this._cfg.storesSource = this._cfg.storesSource ? + extend(this._cfg.storesSource, stores) : + stores; + var versions = db._versions; + var storesSpec = {}; + var dbschema = {}; + versions.forEach(function (version) { + extend(storesSpec, version._cfg.storesSource); + dbschema = (version._cfg.dbschema = {}); + version._parseStoresSpec(storesSpec, dbschema); + }); + db._dbSchema = dbschema; + removeTablesApi(db, [db._allTables, db, db.Transaction.prototype]); + setApiOnPlace(db, [db._allTables, db, db.Transaction.prototype, this._cfg.tables], keys(dbschema), dbschema); + db._storeNames = keys(dbschema); + return this; + }; + Version.prototype.upgrade = function (upgradeFunction) { + this._cfg.contentUpgrade = promisableChain(this._cfg.contentUpgrade || nop, upgradeFunction); + return this; + }; + return Version; +}()); + +function createVersionConstructor(db) { + return makeClassConstructor(Version.prototype, function Version(versionNumber) { + this.db = db; + this._cfg = { + version: versionNumber, + storesSource: null, + dbschema: {}, + tables: {}, + contentUpgrade: null + }; + }); +} + +function getDbNamesTable(indexedDB, IDBKeyRange) { + var dbNamesDB = indexedDB["_dbNamesDB"]; + if (!dbNamesDB) { + dbNamesDB = indexedDB["_dbNamesDB"] = new Dexie$1(DBNAMES_DB, { + addons: [], + indexedDB: indexedDB, + IDBKeyRange: IDBKeyRange, + }); + dbNamesDB.version(1).stores({ dbnames: "name" }); + } + return dbNamesDB.table("dbnames"); +} +function hasDatabasesNative(indexedDB) { + return indexedDB && typeof indexedDB.databases === "function"; +} +function getDatabaseNames(_a) { + var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange; + return hasDatabasesNative(indexedDB) + ? Promise.resolve(indexedDB.databases()).then(function (infos) { + return infos + .map(function (info) { return info.name; }) + .filter(function (name) { return name !== DBNAMES_DB; }); + }) + : getDbNamesTable(indexedDB, IDBKeyRange).toCollection().primaryKeys(); +} +function _onDatabaseCreated(_a, name) { + var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange; + !hasDatabasesNative(indexedDB) && + name !== DBNAMES_DB && + getDbNamesTable(indexedDB, IDBKeyRange).put({ name: name }).catch(nop); +} +function _onDatabaseDeleted(_a, name) { + var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange; + !hasDatabasesNative(indexedDB) && + name !== DBNAMES_DB && + getDbNamesTable(indexedDB, IDBKeyRange).delete(name).catch(nop); +} + +function vip(fn) { + return newScope(function () { + PSD.letThrough = true; + return fn(); + }); +} + +function idbReady() { + var isSafari = !navigator.userAgentData && + /Safari\//.test(navigator.userAgent) && + !/Chrom(e|ium)\//.test(navigator.userAgent); + if (!isSafari || !indexedDB.databases) + return Promise.resolve(); + var intervalId; + return new Promise(function (resolve) { + var tryIdb = function () { return indexedDB.databases().finally(resolve); }; + intervalId = setInterval(tryIdb, 100); + tryIdb(); + }).finally(function () { return clearInterval(intervalId); }); +} + +var _a; +function isEmptyRange(node) { + return !("from" in node); +} +var RangeSet = function (fromOrTree, to) { + if (this) { + extend(this, arguments.length ? { d: 1, from: fromOrTree, to: arguments.length > 1 ? to : fromOrTree } : { d: 0 }); + } + else { + var rv = new RangeSet(); + if (fromOrTree && ("d" in fromOrTree)) { + extend(rv, fromOrTree); + } + return rv; + } +}; +props(RangeSet.prototype, (_a = { + add: function (rangeSet) { + mergeRanges(this, rangeSet); + return this; + }, + addKey: function (key) { + addRange(this, key, key); + return this; + }, + addKeys: function (keys) { + var _this = this; + keys.forEach(function (key) { return addRange(_this, key, key); }); + return this; + }, + hasKey: function (key) { + var node = getRangeSetIterator(this).next(key).value; + return node && cmp(node.from, key) <= 0 && cmp(node.to, key) >= 0; + } + }, + _a[iteratorSymbol] = function () { + return getRangeSetIterator(this); + }, + _a)); +function addRange(target, from, to) { + var diff = cmp(from, to); + if (isNaN(diff)) + return; + if (diff > 0) + throw RangeError(); + if (isEmptyRange(target)) + return extend(target, { from: from, to: to, d: 1 }); + var left = target.l; + var right = target.r; + if (cmp(to, target.from) < 0) { + left + ? addRange(left, from, to) + : (target.l = { from: from, to: to, d: 1, l: null, r: null }); + return rebalance(target); + } + if (cmp(from, target.to) > 0) { + right + ? addRange(right, from, to) + : (target.r = { from: from, to: to, d: 1, l: null, r: null }); + return rebalance(target); + } + if (cmp(from, target.from) < 0) { + target.from = from; + target.l = null; + target.d = right ? right.d + 1 : 1; + } + if (cmp(to, target.to) > 0) { + target.to = to; + target.r = null; + target.d = target.l ? target.l.d + 1 : 1; + } + var rightWasCutOff = !target.r; + if (left && !target.l) { + mergeRanges(target, left); + } + if (right && rightWasCutOff) { + mergeRanges(target, right); + } +} +function mergeRanges(target, newSet) { + function _addRangeSet(target, _a) { + var from = _a.from, to = _a.to, l = _a.l, r = _a.r; + addRange(target, from, to); + if (l) + _addRangeSet(target, l); + if (r) + _addRangeSet(target, r); + } + if (!isEmptyRange(newSet)) + _addRangeSet(target, newSet); +} +function rangesOverlap(rangeSet1, rangeSet2) { + var i1 = getRangeSetIterator(rangeSet2); + var nextResult1 = i1.next(); + if (nextResult1.done) + return false; + var a = nextResult1.value; + var i2 = getRangeSetIterator(rangeSet1); + var nextResult2 = i2.next(a.from); + var b = nextResult2.value; + while (!nextResult1.done && !nextResult2.done) { + if (cmp(b.from, a.to) <= 0 && cmp(b.to, a.from) >= 0) + return true; + cmp(a.from, b.from) < 0 + ? (a = (nextResult1 = i1.next(b.from)).value) + : (b = (nextResult2 = i2.next(a.from)).value); + } + return false; +} +function getRangeSetIterator(node) { + var state = isEmptyRange(node) ? null : { s: 0, n: node }; + return { + next: function (key) { + var keyProvided = arguments.length > 0; + while (state) { + switch (state.s) { + case 0: + state.s = 1; + if (keyProvided) { + while (state.n.l && cmp(key, state.n.from) < 0) + state = { up: state, n: state.n.l, s: 1 }; + } + else { + while (state.n.l) + state = { up: state, n: state.n.l, s: 1 }; + } + case 1: + state.s = 2; + if (!keyProvided || cmp(key, state.n.to) <= 0) + return { value: state.n, done: false }; + case 2: + if (state.n.r) { + state.s = 3; + state = { up: state, n: state.n.r, s: 0 }; + continue; + } + case 3: + state = state.up; + } + } + return { done: true }; + }, + }; +} +function rebalance(target) { + var _a, _b; + var diff = (((_a = target.r) === null || _a === void 0 ? void 0 : _a.d) || 0) - (((_b = target.l) === null || _b === void 0 ? void 0 : _b.d) || 0); + var r = diff > 1 ? "r" : diff < -1 ? "l" : ""; + if (r) { + var l = r === "r" ? "l" : "r"; + var rootClone = __assign({}, target); + var oldRootRight = target[r]; + target.from = oldRootRight.from; + target.to = oldRootRight.to; + target[r] = oldRootRight[r]; + rootClone[r] = oldRootRight[l]; + target[l] = rootClone; + rootClone.d = computeDepth(rootClone); + } + target.d = computeDepth(target); +} +function computeDepth(_a) { + var r = _a.r, l = _a.l; + return (r ? (l ? Math.max(r.d, l.d) : r.d) : l ? l.d : 0) + 1; +} + +function extendObservabilitySet(target, newSet) { + keys(newSet).forEach(function (part) { + if (target[part]) + mergeRanges(target[part], newSet[part]); + else + target[part] = cloneSimpleObjectTree(newSet[part]); + }); + return target; +} + +function obsSetsOverlap(os1, os2) { + return os1.all || os2.all || Object.keys(os1).some(function (key) { return os2[key] && rangesOverlap(os2[key], os1[key]); }); +} + +var cache = {}; + +var unsignaledParts = {}; +var isTaskEnqueued = false; +function signalSubscribersLazily(part, optimistic) { + extendObservabilitySet(unsignaledParts, part); + if (!isTaskEnqueued) { + isTaskEnqueued = true; + setTimeout(function () { + isTaskEnqueued = false; + var parts = unsignaledParts; + unsignaledParts = {}; + signalSubscribersNow(parts, false); + }, 0); + } +} +function signalSubscribersNow(updatedParts, deleteAffectedCacheEntries) { + if (deleteAffectedCacheEntries === void 0) { deleteAffectedCacheEntries = false; } + var queriesToSignal = new Set(); + if (updatedParts.all) { + for (var _i = 0, _a = Object.values(cache); _i < _a.length; _i++) { + var tblCache = _a[_i]; + collectTableSubscribers(tblCache, updatedParts, queriesToSignal, deleteAffectedCacheEntries); + } + } + else { + for (var key in updatedParts) { + var parts = /^idb\:\/\/(.*)\/(.*)\//.exec(key); + if (parts) { + var dbName = parts[1], tableName = parts[2]; + var tblCache = cache["idb://".concat(dbName, "/").concat(tableName)]; + if (tblCache) + collectTableSubscribers(tblCache, updatedParts, queriesToSignal, deleteAffectedCacheEntries); + } + } + } + queriesToSignal.forEach(function (requery) { return requery(); }); +} +function collectTableSubscribers(tblCache, updatedParts, outQueriesToSignal, deleteAffectedCacheEntries) { + var updatedEntryLists = []; + for (var _i = 0, _a = Object.entries(tblCache.queries.query); _i < _a.length; _i++) { + var _b = _a[_i], indexName = _b[0], entries = _b[1]; + var filteredEntries = []; + for (var _c = 0, entries_1 = entries; _c < entries_1.length; _c++) { + var entry = entries_1[_c]; + if (obsSetsOverlap(updatedParts, entry.obsSet)) { + entry.subscribers.forEach(function (requery) { return outQueriesToSignal.add(requery); }); + } + else if (deleteAffectedCacheEntries) { + filteredEntries.push(entry); + } + } + if (deleteAffectedCacheEntries) + updatedEntryLists.push([indexName, filteredEntries]); + } + if (deleteAffectedCacheEntries) { + for (var _d = 0, updatedEntryLists_1 = updatedEntryLists; _d < updatedEntryLists_1.length; _d++) { + var _e = updatedEntryLists_1[_d], indexName = _e[0], filteredEntries = _e[1]; + tblCache.queries.query[indexName] = filteredEntries; + } + } +} + +function dexieOpen(db) { + var state = db._state; + var indexedDB = db._deps.indexedDB; + if (state.isBeingOpened || db.idbdb) + return state.dbReadyPromise.then(function () { return state.dbOpenError ? + rejection(state.dbOpenError) : + db; }); + state.isBeingOpened = true; + state.dbOpenError = null; + state.openComplete = false; + var openCanceller = state.openCanceller; + var nativeVerToOpen = Math.round(db.verno * 10); + var schemaPatchMode = false; + function throwIfCancelled() { + if (state.openCanceller !== openCanceller) + throw new exceptions.DatabaseClosed('db.open() was cancelled'); + } + var resolveDbReady = state.dbReadyResolve, + upgradeTransaction = null, wasCreated = false; + var tryOpenDB = function () { return new DexiePromise(function (resolve, reject) { + throwIfCancelled(); + if (!indexedDB) + throw new exceptions.MissingAPI(); + var dbName = db.name; + var req = state.autoSchema || !nativeVerToOpen ? + indexedDB.open(dbName) : + indexedDB.open(dbName, nativeVerToOpen); + if (!req) + throw new exceptions.MissingAPI(); + req.onerror = eventRejectHandler(reject); + req.onblocked = wrap(db._fireOnBlocked); + req.onupgradeneeded = wrap(function (e) { + upgradeTransaction = req.transaction; + if (state.autoSchema && !db._options.allowEmptyDB) { + req.onerror = preventDefault; + upgradeTransaction.abort(); + req.result.close(); + var delreq = indexedDB.deleteDatabase(dbName); + delreq.onsuccess = delreq.onerror = wrap(function () { + reject(new exceptions.NoSuchDatabase("Database ".concat(dbName, " doesnt exist"))); + }); + } + else { + upgradeTransaction.onerror = eventRejectHandler(reject); + var oldVer = e.oldVersion > Math.pow(2, 62) ? 0 : e.oldVersion; + wasCreated = oldVer < 1; + db.idbdb = req.result; + if (schemaPatchMode) { + patchCurrentVersion(db, upgradeTransaction); + } + runUpgraders(db, oldVer / 10, upgradeTransaction, reject); + } + }, reject); + req.onsuccess = wrap(function () { + upgradeTransaction = null; + var idbdb = db.idbdb = req.result; + var objectStoreNames = slice(idbdb.objectStoreNames); + if (objectStoreNames.length > 0) + try { + var tmpTrans = idbdb.transaction(safariMultiStoreFix(objectStoreNames), 'readonly'); + if (state.autoSchema) + readGlobalSchema(db, idbdb, tmpTrans); + else { + adjustToExistingIndexNames(db, db._dbSchema, tmpTrans); + if (!verifyInstalledSchema(db, tmpTrans) && !schemaPatchMode) { + console.warn("Dexie SchemaDiff: Schema was extended without increasing the number passed to db.version(). Dexie will add missing parts and increment native version number to workaround this."); + idbdb.close(); + nativeVerToOpen = idbdb.version + 1; + schemaPatchMode = true; + return resolve(tryOpenDB()); + } + } + generateMiddlewareStacks(db, tmpTrans); + } + catch (e) { + } + connections.push(db); + idbdb.onversionchange = wrap(function (ev) { + state.vcFired = true; + db.on("versionchange").fire(ev); + }); + idbdb.onclose = wrap(function (ev) { + db.on("close").fire(ev); + }); + if (wasCreated) + _onDatabaseCreated(db._deps, dbName); + resolve(); + }, reject); + }).catch(function (err) { + switch (err === null || err === void 0 ? void 0 : err.name) { + case "UnknownError": + if (state.PR1398_maxLoop > 0) { + state.PR1398_maxLoop--; + console.warn('Dexie: Workaround for Chrome UnknownError on open()'); + return tryOpenDB(); + } + break; + case "VersionError": + if (nativeVerToOpen > 0) { + nativeVerToOpen = 0; + return tryOpenDB(); + } + break; + } + return DexiePromise.reject(err); + }); }; + return DexiePromise.race([ + openCanceller, + (typeof navigator === 'undefined' ? DexiePromise.resolve() : idbReady()).then(tryOpenDB) + ]).then(function () { + throwIfCancelled(); + state.onReadyBeingFired = []; + return DexiePromise.resolve(vip(function () { return db.on.ready.fire(db.vip); })).then(function fireRemainders() { + if (state.onReadyBeingFired.length > 0) { + var remainders_1 = state.onReadyBeingFired.reduce(promisableChain, nop); + state.onReadyBeingFired = []; + return DexiePromise.resolve(vip(function () { return remainders_1(db.vip); })).then(fireRemainders); + } + }); + }).finally(function () { + if (state.openCanceller === openCanceller) { + state.onReadyBeingFired = null; + state.isBeingOpened = false; + } + }).catch(function (err) { + state.dbOpenError = err; + try { + upgradeTransaction && upgradeTransaction.abort(); + } + catch (_a) { } + if (openCanceller === state.openCanceller) { + db._close(); + } + return rejection(err); + }).finally(function () { + state.openComplete = true; + resolveDbReady(); + }).then(function () { + if (wasCreated) { + var everything_1 = {}; + db.tables.forEach(function (table) { + table.schema.indexes.forEach(function (idx) { + if (idx.name) + everything_1["idb://".concat(db.name, "/").concat(table.name, "/").concat(idx.name)] = new RangeSet(-Infinity, [[[]]]); + }); + everything_1["idb://".concat(db.name, "/").concat(table.name, "/")] = everything_1["idb://".concat(db.name, "/").concat(table.name, "/:dels")] = new RangeSet(-Infinity, [[[]]]); + }); + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME).fire(everything_1); + signalSubscribersNow(everything_1, true); + } + return db; + }); +} + +function awaitIterator(iterator) { + var callNext = function (result) { return iterator.next(result); }, doThrow = function (error) { return iterator.throw(error); }, onSuccess = step(callNext), onError = step(doThrow); + function step(getNext) { + return function (val) { + var next = getNext(val), value = next.value; + return next.done ? value : + (!value || typeof value.then !== 'function' ? + isArray(value) ? Promise.all(value).then(onSuccess, onError) : onSuccess(value) : + value.then(onSuccess, onError)); + }; + } + return step(callNext)(); +} + +function extractTransactionArgs(mode, _tableArgs_, scopeFunc) { + var i = arguments.length; + if (i < 2) + throw new exceptions.InvalidArgument("Too few arguments"); + var args = new Array(i - 1); + while (--i) + args[i - 1] = arguments[i]; + scopeFunc = args.pop(); + var tables = flatten(args); + return [mode, tables, scopeFunc]; +} +function enterTransactionScope(db, mode, storeNames, parentTransaction, scopeFunc) { + return DexiePromise.resolve().then(function () { + var transless = PSD.transless || PSD; + var trans = db._createTransaction(mode, storeNames, db._dbSchema, parentTransaction); + trans.explicit = true; + var zoneProps = { + trans: trans, + transless: transless + }; + if (parentTransaction) { + trans.idbtrans = parentTransaction.idbtrans; + } + else { + try { + trans.create(); + trans.idbtrans._explicit = true; + db._state.PR1398_maxLoop = 3; + } + catch (ex) { + if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) { + console.warn('Dexie: Need to reopen db'); + db.close({ disableAutoOpen: false }); + return db.open().then(function () { return enterTransactionScope(db, mode, storeNames, null, scopeFunc); }); + } + return rejection(ex); + } + } + var scopeFuncIsAsync = isAsyncFunction(scopeFunc); + if (scopeFuncIsAsync) { + incrementExpectedAwaits(); + } + var returnValue; + var promiseFollowed = DexiePromise.follow(function () { + returnValue = scopeFunc.call(trans, trans); + if (returnValue) { + if (scopeFuncIsAsync) { + var decrementor = decrementExpectedAwaits.bind(null, null); + returnValue.then(decrementor, decrementor); + } + else if (typeof returnValue.next === 'function' && typeof returnValue.throw === 'function') { + returnValue = awaitIterator(returnValue); + } + } + }, zoneProps); + return (returnValue && typeof returnValue.then === 'function' ? + DexiePromise.resolve(returnValue).then(function (x) { return trans.active ? + x + : rejection(new exceptions.PrematureCommit("Transaction committed too early. See http://bit.ly/2kdckMn")); }) + : promiseFollowed.then(function () { return returnValue; })).then(function (x) { + if (parentTransaction) + trans._resolve(); + return trans._completion.then(function () { return x; }); + }).catch(function (e) { + trans._reject(e); + return rejection(e); + }); + }); +} + +function pad(a, value, count) { + var result = isArray(a) ? a.slice() : [a]; + for (var i = 0; i < count; ++i) + result.push(value); + return result; +} +function createVirtualIndexMiddleware(down) { + return __assign(__assign({}, down), { table: function (tableName) { + var table = down.table(tableName); + var schema = table.schema; + var indexLookup = {}; + var allVirtualIndexes = []; + function addVirtualIndexes(keyPath, keyTail, lowLevelIndex) { + var keyPathAlias = getKeyPathAlias(keyPath); + var indexList = (indexLookup[keyPathAlias] = indexLookup[keyPathAlias] || []); + var keyLength = keyPath == null ? 0 : typeof keyPath === 'string' ? 1 : keyPath.length; + var isVirtual = keyTail > 0; + var virtualIndex = __assign(__assign({}, lowLevelIndex), { name: isVirtual + ? "".concat(keyPathAlias, "(virtual-from:").concat(lowLevelIndex.name, ")") + : lowLevelIndex.name, lowLevelIndex: lowLevelIndex, isVirtual: isVirtual, keyTail: keyTail, keyLength: keyLength, extractKey: getKeyExtractor(keyPath), unique: !isVirtual && lowLevelIndex.unique }); + indexList.push(virtualIndex); + if (!virtualIndex.isPrimaryKey) { + allVirtualIndexes.push(virtualIndex); + } + if (keyLength > 1) { + var virtualKeyPath = keyLength === 2 ? + keyPath[0] : + keyPath.slice(0, keyLength - 1); + addVirtualIndexes(virtualKeyPath, keyTail + 1, lowLevelIndex); + } + indexList.sort(function (a, b) { return a.keyTail - b.keyTail; }); + return virtualIndex; + } + var primaryKey = addVirtualIndexes(schema.primaryKey.keyPath, 0, schema.primaryKey); + indexLookup[":id"] = [primaryKey]; + for (var _i = 0, _a = schema.indexes; _i < _a.length; _i++) { + var index = _a[_i]; + addVirtualIndexes(index.keyPath, 0, index); + } + function findBestIndex(keyPath) { + var result = indexLookup[getKeyPathAlias(keyPath)]; + return result && result[0]; + } + function translateRange(range, keyTail) { + return { + type: range.type === 1 ? + 2 : + range.type, + lower: pad(range.lower, range.lowerOpen ? down.MAX_KEY : down.MIN_KEY, keyTail), + lowerOpen: true, + upper: pad(range.upper, range.upperOpen ? down.MIN_KEY : down.MAX_KEY, keyTail), + upperOpen: true + }; + } + function translateRequest(req) { + var index = req.query.index; + return index.isVirtual ? __assign(__assign({}, req), { query: { + index: index.lowLevelIndex, + range: translateRange(req.query.range, index.keyTail) + } }) : req; + } + var result = __assign(__assign({}, table), { schema: __assign(__assign({}, schema), { primaryKey: primaryKey, indexes: allVirtualIndexes, getIndexByKeyPath: findBestIndex }), count: function (req) { + return table.count(translateRequest(req)); + }, query: function (req) { + return table.query(translateRequest(req)); + }, openCursor: function (req) { + var _a = req.query.index, keyTail = _a.keyTail, isVirtual = _a.isVirtual, keyLength = _a.keyLength; + if (!isVirtual) + return table.openCursor(req); + function createVirtualCursor(cursor) { + function _continue(key) { + key != null ? + cursor.continue(pad(key, req.reverse ? down.MAX_KEY : down.MIN_KEY, keyTail)) : + req.unique ? + cursor.continue(cursor.key.slice(0, keyLength) + .concat(req.reverse + ? down.MIN_KEY + : down.MAX_KEY, keyTail)) : + cursor.continue(); + } + var virtualCursor = Object.create(cursor, { + continue: { value: _continue }, + continuePrimaryKey: { + value: function (key, primaryKey) { + cursor.continuePrimaryKey(pad(key, down.MAX_KEY, keyTail), primaryKey); + } + }, + primaryKey: { + get: function () { + return cursor.primaryKey; + } + }, + key: { + get: function () { + var key = cursor.key; + return keyLength === 1 ? + key[0] : + key.slice(0, keyLength); + } + }, + value: { + get: function () { + return cursor.value; + } + } + }); + return virtualCursor; + } + return table.openCursor(translateRequest(req)) + .then(function (cursor) { return cursor && createVirtualCursor(cursor); }); + } }); + return result; + } }); +} +var virtualIndexMiddleware = { + stack: "dbcore", + name: "VirtualIndexMiddleware", + level: 1, + create: createVirtualIndexMiddleware +}; + +function getObjectDiff(a, b, rv, prfx) { + rv = rv || {}; + prfx = prfx || ''; + keys(a).forEach(function (prop) { + if (!hasOwn(b, prop)) { + rv[prfx + prop] = undefined; + } + else { + var ap = a[prop], bp = b[prop]; + if (typeof ap === 'object' && typeof bp === 'object' && ap && bp) { + var apTypeName = toStringTag(ap); + var bpTypeName = toStringTag(bp); + if (apTypeName !== bpTypeName) { + rv[prfx + prop] = b[prop]; + } + else if (apTypeName === 'Object') { + getObjectDiff(ap, bp, rv, prfx + prop + '.'); + } + else if (ap !== bp) { + rv[prfx + prop] = b[prop]; + } + } + else if (ap !== bp) + rv[prfx + prop] = b[prop]; + } + }); + keys(b).forEach(function (prop) { + if (!hasOwn(a, prop)) { + rv[prfx + prop] = b[prop]; + } + }); + return rv; +} + +function getEffectiveKeys(primaryKey, req) { + if (req.type === 'delete') + return req.keys; + return req.keys || req.values.map(primaryKey.extractKey); +} + +var hooksMiddleware = { + stack: "dbcore", + name: "HooksMiddleware", + level: 2, + create: function (downCore) { return (__assign(__assign({}, downCore), { table: function (tableName) { + var downTable = downCore.table(tableName); + var primaryKey = downTable.schema.primaryKey; + var tableMiddleware = __assign(__assign({}, downTable), { mutate: function (req) { + var dxTrans = PSD.trans; + var _a = dxTrans.table(tableName).hook, deleting = _a.deleting, creating = _a.creating, updating = _a.updating; + switch (req.type) { + case 'add': + if (creating.fire === nop) + break; + return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true); + case 'put': + if (creating.fire === nop && updating.fire === nop) + break; + return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true); + case 'delete': + if (deleting.fire === nop) + break; + return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true); + case 'deleteRange': + if (deleting.fire === nop) + break; + return dxTrans._promise('readwrite', function () { return deleteRange(req); }, true); + } + return downTable.mutate(req); + function addPutOrDelete(req) { + var dxTrans = PSD.trans; + var keys = req.keys || getEffectiveKeys(primaryKey, req); + if (!keys) + throw new Error("Keys missing"); + req = req.type === 'add' || req.type === 'put' ? __assign(__assign({}, req), { keys: keys }) : __assign({}, req); + if (req.type !== 'delete') + req.values = __spreadArray([], req.values, true); + if (req.keys) + req.keys = __spreadArray([], req.keys, true); + return getExistingValues(downTable, req, keys).then(function (existingValues) { + var contexts = keys.map(function (key, i) { + var existingValue = existingValues[i]; + var ctx = { onerror: null, onsuccess: null }; + if (req.type === 'delete') { + deleting.fire.call(ctx, key, existingValue, dxTrans); + } + else if (req.type === 'add' || existingValue === undefined) { + var generatedPrimaryKey = creating.fire.call(ctx, key, req.values[i], dxTrans); + if (key == null && generatedPrimaryKey != null) { + key = generatedPrimaryKey; + req.keys[i] = key; + if (!primaryKey.outbound) { + setByKeyPath(req.values[i], primaryKey.keyPath, key); + } + } + } + else { + var objectDiff = getObjectDiff(existingValue, req.values[i]); + var additionalChanges_1 = updating.fire.call(ctx, objectDiff, key, existingValue, dxTrans); + if (additionalChanges_1) { + var requestedValue_1 = req.values[i]; + Object.keys(additionalChanges_1).forEach(function (keyPath) { + if (hasOwn(requestedValue_1, keyPath)) { + requestedValue_1[keyPath] = additionalChanges_1[keyPath]; + } + else { + setByKeyPath(requestedValue_1, keyPath, additionalChanges_1[keyPath]); + } + }); + } + } + return ctx; + }); + return downTable.mutate(req).then(function (_a) { + var failures = _a.failures, results = _a.results, numFailures = _a.numFailures, lastResult = _a.lastResult; + for (var i = 0; i < keys.length; ++i) { + var primKey = results ? results[i] : keys[i]; + var ctx = contexts[i]; + if (primKey == null) { + ctx.onerror && ctx.onerror(failures[i]); + } + else { + ctx.onsuccess && ctx.onsuccess(req.type === 'put' && existingValues[i] ? + req.values[i] : + primKey + ); + } + } + return { failures: failures, results: results, numFailures: numFailures, lastResult: lastResult }; + }).catch(function (error) { + contexts.forEach(function (ctx) { return ctx.onerror && ctx.onerror(error); }); + return Promise.reject(error); + }); + }); + } + function deleteRange(req) { + return deleteNextChunk(req.trans, req.range, 10000); + } + function deleteNextChunk(trans, range, limit) { + return downTable.query({ trans: trans, values: false, query: { index: primaryKey, range: range }, limit: limit }) + .then(function (_a) { + var result = _a.result; + return addPutOrDelete({ type: 'delete', keys: result, trans: trans }).then(function (res) { + if (res.numFailures > 0) + return Promise.reject(res.failures[0]); + if (result.length < limit) { + return { failures: [], numFailures: 0, lastResult: undefined }; + } + else { + return deleteNextChunk(trans, __assign(__assign({}, range), { lower: result[result.length - 1], lowerOpen: true }), limit); + } + }); + }); + } + } }); + return tableMiddleware; + } })); } +}; +function getExistingValues(table, req, effectiveKeys) { + return req.type === "add" + ? Promise.resolve([]) + : table.getMany({ trans: req.trans, keys: effectiveKeys, cache: "immutable" }); +} + +function getFromTransactionCache(keys, cache, clone) { + try { + if (!cache) + return null; + if (cache.keys.length < keys.length) + return null; + var result = []; + for (var i = 0, j = 0; i < cache.keys.length && j < keys.length; ++i) { + if (cmp(cache.keys[i], keys[j]) !== 0) + continue; + result.push(clone ? deepClone(cache.values[i]) : cache.values[i]); + ++j; + } + return result.length === keys.length ? result : null; + } + catch (_a) { + return null; + } +} +var cacheExistingValuesMiddleware = { + stack: "dbcore", + level: -1, + create: function (core) { + return { + table: function (tableName) { + var table = core.table(tableName); + return __assign(__assign({}, table), { getMany: function (req) { + if (!req.cache) { + return table.getMany(req); + } + var cachedResult = getFromTransactionCache(req.keys, req.trans["_cache"], req.cache === "clone"); + if (cachedResult) { + return DexiePromise.resolve(cachedResult); + } + return table.getMany(req).then(function (res) { + req.trans["_cache"] = { + keys: req.keys, + values: req.cache === "clone" ? deepClone(res) : res, + }; + return res; + }); + }, mutate: function (req) { + if (req.type !== "add") + req.trans["_cache"] = null; + return table.mutate(req); + } }); + }, + }; + }, +}; + +function isCachableContext(ctx, table) { + return (ctx.trans.mode === 'readonly' && + !!ctx.subscr && + !ctx.trans.explicit && + ctx.trans.db._options.cache !== 'disabled' && + !table.schema.primaryKey.outbound); +} + +function isCachableRequest(type, req) { + switch (type) { + case 'query': + return req.values && !req.unique; + case 'get': + return false; + case 'getMany': + return false; + case 'count': + return false; + case 'openCursor': + return false; + } +} + +var observabilityMiddleware = { + stack: "dbcore", + level: 0, + name: "Observability", + create: function (core) { + var dbName = core.schema.name; + var FULL_RANGE = new RangeSet(core.MIN_KEY, core.MAX_KEY); + return __assign(__assign({}, core), { transaction: function (stores, mode, options) { + if (PSD.subscr && mode !== 'readonly') { + throw new exceptions.ReadOnly("Readwrite transaction in liveQuery context. Querier source: ".concat(PSD.querier)); + } + return core.transaction(stores, mode, options); + }, table: function (tableName) { + var table = core.table(tableName); + var schema = table.schema; + var primaryKey = schema.primaryKey, indexes = schema.indexes; + var extractKey = primaryKey.extractKey, outbound = primaryKey.outbound; + var indexesWithAutoIncPK = primaryKey.autoIncrement && indexes.filter(function (index) { return index.compound && index.keyPath.includes(primaryKey.keyPath); }); + var tableClone = __assign(__assign({}, table), { mutate: function (req) { + var _a, _b; + var trans = req.trans; + var mutatedParts = req.mutatedParts || (req.mutatedParts = {}); + var getRangeSet = function (indexName) { + var part = "idb://".concat(dbName, "/").concat(tableName, "/").concat(indexName); + return (mutatedParts[part] || + (mutatedParts[part] = new RangeSet())); + }; + var pkRangeSet = getRangeSet(""); + var delsRangeSet = getRangeSet(":dels"); + var type = req.type; + var _c = req.type === "deleteRange" + ? [req.range] + : req.type === "delete" + ? [req.keys] + : req.values.length < 50 + ? [getEffectiveKeys(primaryKey, req).filter(function (id) { return id; }), req.values] + : [], keys = _c[0], newObjs = _c[1]; + var oldCache = req.trans["_cache"]; + if (isArray(keys)) { + pkRangeSet.addKeys(keys); + var oldObjs = type === 'delete' || keys.length === newObjs.length ? getFromTransactionCache(keys, oldCache) : null; + if (!oldObjs) { + delsRangeSet.addKeys(keys); + } + if (oldObjs || newObjs) { + trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs); + } + } + else if (keys) { + var range = { + from: (_a = keys.lower) !== null && _a !== void 0 ? _a : core.MIN_KEY, + to: (_b = keys.upper) !== null && _b !== void 0 ? _b : core.MAX_KEY + }; + delsRangeSet.add(range); + pkRangeSet.add(range); + } + else { + pkRangeSet.add(FULL_RANGE); + delsRangeSet.add(FULL_RANGE); + schema.indexes.forEach(function (idx) { return getRangeSet(idx.name).add(FULL_RANGE); }); + } + return table.mutate(req).then(function (res) { + if (keys && (req.type === 'add' || req.type === 'put')) { + pkRangeSet.addKeys(res.results); + if (indexesWithAutoIncPK) { + indexesWithAutoIncPK.forEach(function (idx) { + var idxVals = req.values.map(function (v) { return idx.extractKey(v); }); + var pkPos = idx.keyPath.findIndex(function (prop) { return prop === primaryKey.keyPath; }); + for (var i = 0, len = res.results.length; i < len; ++i) { + idxVals[i][pkPos] = res.results[i]; + } + getRangeSet(idx.name).addKeys(idxVals); + }); + } + } + trans.mutatedParts = extendObservabilitySet(trans.mutatedParts || {}, mutatedParts); + return res; + }); + } }); + var getRange = function (_a) { + var _b, _c; + var _d = _a.query, index = _d.index, range = _d.range; + return [ + index, + new RangeSet((_b = range.lower) !== null && _b !== void 0 ? _b : core.MIN_KEY, (_c = range.upper) !== null && _c !== void 0 ? _c : core.MAX_KEY), + ]; + }; + var readSubscribers = { + get: function (req) { return [primaryKey, new RangeSet(req.key)]; }, + getMany: function (req) { return [primaryKey, new RangeSet().addKeys(req.keys)]; }, + count: getRange, + query: getRange, + openCursor: getRange, + }; + keys(readSubscribers).forEach(function (method) { + tableClone[method] = function (req) { + var subscr = PSD.subscr; + var isLiveQuery = !!subscr; + var cachable = isCachableContext(PSD, table) && isCachableRequest(method, req); + var obsSet = cachable + ? req.obsSet = {} + : subscr; + if (isLiveQuery) { + var getRangeSet = function (indexName) { + var part = "idb://".concat(dbName, "/").concat(tableName, "/").concat(indexName); + return (obsSet[part] || + (obsSet[part] = new RangeSet())); + }; + var pkRangeSet_1 = getRangeSet(""); + var delsRangeSet_1 = getRangeSet(":dels"); + var _a = readSubscribers[method](req), queriedIndex = _a[0], queriedRanges = _a[1]; + if (method === 'query' && queriedIndex.isPrimaryKey && !req.values) { + delsRangeSet_1.add(queriedRanges); + } + else { + getRangeSet(queriedIndex.name || "").add(queriedRanges); + } + if (!queriedIndex.isPrimaryKey) { + if (method === "count") { + delsRangeSet_1.add(FULL_RANGE); + } + else { + var keysPromise_1 = method === "query" && + outbound && + req.values && + table.query(__assign(__assign({}, req), { values: false })); + return table[method].apply(this, arguments).then(function (res) { + if (method === "query") { + if (outbound && req.values) { + return keysPromise_1.then(function (_a) { + var resultingKeys = _a.result; + pkRangeSet_1.addKeys(resultingKeys); + return res; + }); + } + var pKeys = req.values + ? res.result.map(extractKey) + : res.result; + if (req.values) { + pkRangeSet_1.addKeys(pKeys); + } + else { + delsRangeSet_1.addKeys(pKeys); + } + } + else if (method === "openCursor") { + var cursor_1 = res; + var wantValues_1 = req.values; + return (cursor_1 && + Object.create(cursor_1, { + key: { + get: function () { + delsRangeSet_1.addKey(cursor_1.primaryKey); + return cursor_1.key; + }, + }, + primaryKey: { + get: function () { + var pkey = cursor_1.primaryKey; + delsRangeSet_1.addKey(pkey); + return pkey; + }, + }, + value: { + get: function () { + wantValues_1 && pkRangeSet_1.addKey(cursor_1.primaryKey); + return cursor_1.value; + }, + }, + })); + } + return res; + }); + } + } + } + return table[method].apply(this, arguments); + }; + }); + return tableClone; + } }); + }, +}; +function trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs) { + function addAffectedIndex(ix) { + var rangeSet = getRangeSet(ix.name || ""); + function extractKey(obj) { + return obj != null ? ix.extractKey(obj) : null; + } + var addKeyOrKeys = function (key) { return ix.multiEntry && isArray(key) + ? key.forEach(function (key) { return rangeSet.addKey(key); }) + : rangeSet.addKey(key); }; + (oldObjs || newObjs).forEach(function (_, i) { + var oldKey = oldObjs && extractKey(oldObjs[i]); + var newKey = newObjs && extractKey(newObjs[i]); + if (cmp(oldKey, newKey) !== 0) { + if (oldKey != null) + addKeyOrKeys(oldKey); + if (newKey != null) + addKeyOrKeys(newKey); + } + }); + } + schema.indexes.forEach(addAffectedIndex); +} + +function adjustOptimisticFromFailures(tblCache, req, res) { + if (res.numFailures === 0) + return req; + if (req.type === 'deleteRange') { + return null; + } + var numBulkOps = req.keys + ? req.keys.length + : 'values' in req && req.values + ? req.values.length + : 1; + if (res.numFailures === numBulkOps) { + return null; + } + var clone = __assign({}, req); + if (isArray(clone.keys)) { + clone.keys = clone.keys.filter(function (_, i) { return !(i in res.failures); }); + } + if ('values' in clone && isArray(clone.values)) { + clone.values = clone.values.filter(function (_, i) { return !(i in res.failures); }); + } + return clone; +} + +function isAboveLower(key, range) { + return range.lower === undefined + ? true + : range.lowerOpen + ? cmp(key, range.lower) > 0 + : cmp(key, range.lower) >= 0; +} +function isBelowUpper(key, range) { + return range.upper === undefined + ? true + : range.upperOpen + ? cmp(key, range.upper) < 0 + : cmp(key, range.upper) <= 0; +} +function isWithinRange(key, range) { + return isAboveLower(key, range) && isBelowUpper(key, range); +} + +function applyOptimisticOps(result, req, ops, table, cacheEntry, immutable) { + if (!ops || ops.length === 0) + return result; + var index = req.query.index; + var multiEntry = index.multiEntry; + var queryRange = req.query.range; + var primaryKey = table.schema.primaryKey; + var extractPrimKey = primaryKey.extractKey; + var extractIndex = index.extractKey; + var extractLowLevelIndex = (index.lowLevelIndex || index).extractKey; + var finalResult = ops.reduce(function (result, op) { + var modifedResult = result; + var includedValues = []; + if (op.type === 'add' || op.type === 'put') { + var includedPKs = new RangeSet(); + for (var i = op.values.length - 1; i >= 0; --i) { + var value = op.values[i]; + var pk = extractPrimKey(value); + if (includedPKs.hasKey(pk)) + continue; + var key = extractIndex(value); + if (multiEntry && isArray(key) + ? key.some(function (k) { return isWithinRange(k, queryRange); }) + : isWithinRange(key, queryRange)) { + includedPKs.addKey(pk); + includedValues.push(value); + } + } + } + switch (op.type) { + case 'add': { + var existingKeys_1 = new RangeSet().addKeys(req.values ? result.map(function (v) { return extractPrimKey(v); }) : result); + modifedResult = result.concat(req.values + ? includedValues.filter(function (v) { + var key = extractPrimKey(v); + if (existingKeys_1.hasKey(key)) + return false; + existingKeys_1.addKey(key); + return true; + }) + : includedValues + .map(function (v) { return extractPrimKey(v); }) + .filter(function (k) { + if (existingKeys_1.hasKey(k)) + return false; + existingKeys_1.addKey(k); + return true; + })); + break; + } + case 'put': { + var keySet_1 = new RangeSet().addKeys(op.values.map(function (v) { return extractPrimKey(v); })); + modifedResult = result + .filter( + function (item) { return !keySet_1.hasKey(req.values ? extractPrimKey(item) : item); }) + .concat( + req.values + ? includedValues + : includedValues.map(function (v) { return extractPrimKey(v); })); + break; + } + case 'delete': + var keysToDelete_1 = new RangeSet().addKeys(op.keys); + modifedResult = result.filter(function (item) { + return !keysToDelete_1.hasKey(req.values ? extractPrimKey(item) : item); + }); + break; + case 'deleteRange': + var range_1 = op.range; + modifedResult = result.filter(function (item) { return !isWithinRange(extractPrimKey(item), range_1); }); + break; + } + return modifedResult; + }, result); + if (finalResult === result) + return result; + finalResult.sort(function (a, b) { + return cmp(extractLowLevelIndex(a), extractLowLevelIndex(b)) || + cmp(extractPrimKey(a), extractPrimKey(b)); + }); + if (req.limit && req.limit < Infinity) { + if (finalResult.length > req.limit) { + finalResult.length = req.limit; + } + else if (result.length === req.limit && finalResult.length < req.limit) { + cacheEntry.dirty = true; + } + } + return immutable ? Object.freeze(finalResult) : finalResult; +} + +function areRangesEqual(r1, r2) { + return (cmp(r1.lower, r2.lower) === 0 && + cmp(r1.upper, r2.upper) === 0 && + !!r1.lowerOpen === !!r2.lowerOpen && + !!r1.upperOpen === !!r2.upperOpen); +} + +function compareLowers(lower1, lower2, lowerOpen1, lowerOpen2) { + if (lower1 === undefined) + return lower2 !== undefined ? -1 : 0; + if (lower2 === undefined) + return 1; + var c = cmp(lower1, lower2); + if (c === 0) { + if (lowerOpen1 && lowerOpen2) + return 0; + if (lowerOpen1) + return 1; + if (lowerOpen2) + return -1; + } + return c; +} +function compareUppers(upper1, upper2, upperOpen1, upperOpen2) { + if (upper1 === undefined) + return upper2 !== undefined ? 1 : 0; + if (upper2 === undefined) + return -1; + var c = cmp(upper1, upper2); + if (c === 0) { + if (upperOpen1 && upperOpen2) + return 0; + if (upperOpen1) + return -1; + if (upperOpen2) + return 1; + } + return c; +} +function isSuperRange(r1, r2) { + return (compareLowers(r1.lower, r2.lower, r1.lowerOpen, r2.lowerOpen) <= 0 && + compareUppers(r1.upper, r2.upper, r1.upperOpen, r2.upperOpen) >= 0); +} + +function findCompatibleQuery(dbName, tableName, type, req) { + var tblCache = cache["idb://".concat(dbName, "/").concat(tableName)]; + if (!tblCache) + return []; + var queries = tblCache.queries[type]; + if (!queries) + return [null, false, tblCache, null]; + var indexName = req.query ? req.query.index.name : null; + var entries = queries[indexName || '']; + if (!entries) + return [null, false, tblCache, null]; + switch (type) { + case 'query': + var equalEntry = entries.find(function (entry) { + return entry.req.limit === req.limit && + entry.req.values === req.values && + areRangesEqual(entry.req.query.range, req.query.range); + }); + if (equalEntry) + return [ + equalEntry, + true, + tblCache, + entries, + ]; + var superEntry = entries.find(function (entry) { + var limit = 'limit' in entry.req ? entry.req.limit : Infinity; + return (limit >= req.limit && + (req.values ? entry.req.values : true) && + isSuperRange(entry.req.query.range, req.query.range)); + }); + return [superEntry, false, tblCache, entries]; + case 'count': + var countQuery = entries.find(function (entry) { + return areRangesEqual(entry.req.query.range, req.query.range); + }); + return [countQuery, !!countQuery, tblCache, entries]; + } +} + +function subscribeToCacheEntry(cacheEntry, container, requery, signal) { + cacheEntry.subscribers.add(requery); + signal.addEventListener("abort", function () { + cacheEntry.subscribers.delete(requery); + if (cacheEntry.subscribers.size === 0) { + enqueForDeletion(cacheEntry, container); + } + }); +} +function enqueForDeletion(cacheEntry, container) { + setTimeout(function () { + if (cacheEntry.subscribers.size === 0) { + delArrayItem(container, cacheEntry); + } + }, 3000); +} + +var cacheMiddleware = { + stack: 'dbcore', + level: 0, + name: 'Cache', + create: function (core) { + var dbName = core.schema.name; + var coreMW = __assign(__assign({}, core), { transaction: function (stores, mode, options) { + var idbtrans = core.transaction(stores, mode, options); + if (mode === 'readwrite') { + var ac_1 = new AbortController(); + var signal = ac_1.signal; + var endTransaction = function (wasCommitted) { return function () { + ac_1.abort(); + if (mode === 'readwrite') { + var affectedSubscribers_1 = new Set(); + for (var _i = 0, stores_1 = stores; _i < stores_1.length; _i++) { + var storeName = stores_1[_i]; + var tblCache = cache["idb://".concat(dbName, "/").concat(storeName)]; + if (tblCache) { + var table = core.table(storeName); + var ops = tblCache.optimisticOps.filter(function (op) { return op.trans === idbtrans; }); + if (idbtrans._explicit && wasCommitted && idbtrans.mutatedParts) { + for (var _a = 0, _b = Object.values(tblCache.queries.query); _a < _b.length; _a++) { + var entries = _b[_a]; + for (var _c = 0, _d = entries.slice(); _c < _d.length; _c++) { + var entry = _d[_c]; + if (obsSetsOverlap(entry.obsSet, idbtrans.mutatedParts)) { + delArrayItem(entries, entry); + entry.subscribers.forEach(function (requery) { return affectedSubscribers_1.add(requery); }); + } + } + } + } + else if (ops.length > 0) { + tblCache.optimisticOps = tblCache.optimisticOps.filter(function (op) { return op.trans !== idbtrans; }); + for (var _e = 0, _f = Object.values(tblCache.queries.query); _e < _f.length; _e++) { + var entries = _f[_e]; + for (var _g = 0, _h = entries.slice(); _g < _h.length; _g++) { + var entry = _h[_g]; + if (entry.res != null && + idbtrans.mutatedParts +) { + if (wasCommitted && !entry.dirty) { + var freezeResults = Object.isFrozen(entry.res); + var modRes = applyOptimisticOps(entry.res, entry.req, ops, table, entry, freezeResults); + if (entry.dirty) { + delArrayItem(entries, entry); + entry.subscribers.forEach(function (requery) { return affectedSubscribers_1.add(requery); }); + } + else if (modRes !== entry.res) { + entry.res = modRes; + entry.promise = DexiePromise.resolve({ result: modRes }); + } + } + else { + if (entry.dirty) { + delArrayItem(entries, entry); + } + entry.subscribers.forEach(function (requery) { return affectedSubscribers_1.add(requery); }); + } + } + } + } + } + } + } + affectedSubscribers_1.forEach(function (requery) { return requery(); }); + } + }; }; + idbtrans.addEventListener('abort', endTransaction(false), { + signal: signal, + }); + idbtrans.addEventListener('error', endTransaction(false), { + signal: signal, + }); + idbtrans.addEventListener('complete', endTransaction(true), { + signal: signal, + }); + } + return idbtrans; + }, table: function (tableName) { + var downTable = core.table(tableName); + var primKey = downTable.schema.primaryKey; + var tableMW = __assign(__assign({}, downTable), { mutate: function (req) { + var trans = PSD.trans; + if (primKey.outbound || + trans.db._options.cache === 'disabled' || + trans.explicit || + trans.idbtrans.mode !== 'readwrite' + ) { + return downTable.mutate(req); + } + var tblCache = cache["idb://".concat(dbName, "/").concat(tableName)]; + if (!tblCache) + return downTable.mutate(req); + var promise = downTable.mutate(req); + if ((req.type === 'add' || req.type === 'put') && (req.values.length >= 50 || getEffectiveKeys(primKey, req).some(function (key) { return key == null; }))) { + promise.then(function (res) { + var reqWithResolvedKeys = __assign(__assign({}, req), { values: req.values.map(function (value, i) { + var _a; + if (res.failures[i]) + return value; + var valueWithKey = ((_a = primKey.keyPath) === null || _a === void 0 ? void 0 : _a.includes('.')) + ? deepClone(value) + : __assign({}, value); + setByKeyPath(valueWithKey, primKey.keyPath, res.results[i]); + return valueWithKey; + }) }); + var adjustedReq = adjustOptimisticFromFailures(tblCache, reqWithResolvedKeys, res); + tblCache.optimisticOps.push(adjustedReq); + queueMicrotask(function () { return req.mutatedParts && signalSubscribersLazily(req.mutatedParts); }); + }); + } + else { + tblCache.optimisticOps.push(req); + req.mutatedParts && signalSubscribersLazily(req.mutatedParts); + promise.then(function (res) { + if (res.numFailures > 0) { + delArrayItem(tblCache.optimisticOps, req); + var adjustedReq = adjustOptimisticFromFailures(tblCache, req, res); + if (adjustedReq) { + tblCache.optimisticOps.push(adjustedReq); + } + req.mutatedParts && signalSubscribersLazily(req.mutatedParts); + } + }); + promise.catch(function () { + delArrayItem(tblCache.optimisticOps, req); + req.mutatedParts && signalSubscribersLazily(req.mutatedParts); + }); + } + return promise; + }, query: function (req) { + var _a; + if (!isCachableContext(PSD, downTable) || !isCachableRequest("query", req)) + return downTable.query(req); + var freezeResults = ((_a = PSD.trans) === null || _a === void 0 ? void 0 : _a.db._options.cache) === 'immutable'; + var _b = PSD, requery = _b.requery, signal = _b.signal; + var _c = findCompatibleQuery(dbName, tableName, 'query', req), cacheEntry = _c[0], exactMatch = _c[1], tblCache = _c[2], container = _c[3]; + if (cacheEntry && exactMatch) { + cacheEntry.obsSet = req.obsSet; + } + else { + var promise = downTable.query(req).then(function (res) { + var result = res.result; + if (cacheEntry) + cacheEntry.res = result; + if (freezeResults) { + for (var i = 0, l = result.length; i < l; ++i) { + Object.freeze(result[i]); + } + Object.freeze(result); + } + else { + res.result = deepClone(result); + } + return res; + }).catch(function (error) { + if (container && cacheEntry) + delArrayItem(container, cacheEntry); + return Promise.reject(error); + }); + cacheEntry = { + obsSet: req.obsSet, + promise: promise, + subscribers: new Set(), + type: 'query', + req: req, + dirty: false, + }; + if (container) { + container.push(cacheEntry); + } + else { + container = [cacheEntry]; + if (!tblCache) { + tblCache = cache["idb://".concat(dbName, "/").concat(tableName)] = { + queries: { + query: {}, + count: {}, + }, + objs: new Map(), + optimisticOps: [], + unsignaledParts: {} + }; + } + tblCache.queries.query[req.query.index.name || ''] = container; + } + } + subscribeToCacheEntry(cacheEntry, container, requery, signal); + return cacheEntry.promise.then(function (res) { + return { + result: applyOptimisticOps(res.result, req, tblCache === null || tblCache === void 0 ? void 0 : tblCache.optimisticOps, downTable, cacheEntry, freezeResults), + }; + }); + } }); + return tableMW; + } }); + return coreMW; + }, +}; + +function vipify(target, vipDb) { + return new Proxy(target, { + get: function (target, prop, receiver) { + if (prop === 'db') + return vipDb; + return Reflect.get(target, prop, receiver); + } + }); +} + +var Dexie$1 = (function () { + function Dexie(name, options) { + var _this = this; + this._middlewares = {}; + this.verno = 0; + var deps = Dexie.dependencies; + this._options = options = __assign({ + addons: Dexie.addons, autoOpen: true, + indexedDB: deps.indexedDB, IDBKeyRange: deps.IDBKeyRange, cache: 'cloned' }, options); + this._deps = { + indexedDB: options.indexedDB, + IDBKeyRange: options.IDBKeyRange + }; + var addons = options.addons; + this._dbSchema = {}; + this._versions = []; + this._storeNames = []; + this._allTables = {}; + this.idbdb = null; + this._novip = this; + var state = { + dbOpenError: null, + isBeingOpened: false, + onReadyBeingFired: null, + openComplete: false, + dbReadyResolve: nop, + dbReadyPromise: null, + cancelOpen: nop, + openCanceller: null, + autoSchema: true, + PR1398_maxLoop: 3, + autoOpen: options.autoOpen, + }; + state.dbReadyPromise = new DexiePromise(function (resolve) { + state.dbReadyResolve = resolve; + }); + state.openCanceller = new DexiePromise(function (_, reject) { + state.cancelOpen = reject; + }); + this._state = state; + this.name = name; + this.on = Events(this, "populate", "blocked", "versionchange", "close", { ready: [promisableChain, nop] }); + this.on.ready.subscribe = override(this.on.ready.subscribe, function (subscribe) { + return function (subscriber, bSticky) { + Dexie.vip(function () { + var state = _this._state; + if (state.openComplete) { + if (!state.dbOpenError) + DexiePromise.resolve().then(subscriber); + if (bSticky) + subscribe(subscriber); + } + else if (state.onReadyBeingFired) { + state.onReadyBeingFired.push(subscriber); + if (bSticky) + subscribe(subscriber); + } + else { + subscribe(subscriber); + var db_1 = _this; + if (!bSticky) + subscribe(function unsubscribe() { + db_1.on.ready.unsubscribe(subscriber); + db_1.on.ready.unsubscribe(unsubscribe); + }); + } + }); + }; + }); + this.Collection = createCollectionConstructor(this); + this.Table = createTableConstructor(this); + this.Transaction = createTransactionConstructor(this); + this.Version = createVersionConstructor(this); + this.WhereClause = createWhereClauseConstructor(this); + this.on("versionchange", function (ev) { + if (ev.newVersion > 0) + console.warn("Another connection wants to upgrade database '".concat(_this.name, "'. Closing db now to resume the upgrade.")); + else + console.warn("Another connection wants to delete database '".concat(_this.name, "'. Closing db now to resume the delete request.")); + _this.close({ disableAutoOpen: false }); + }); + this.on("blocked", function (ev) { + if (!ev.newVersion || ev.newVersion < ev.oldVersion) + console.warn("Dexie.delete('".concat(_this.name, "') was blocked")); + else + console.warn("Upgrade '".concat(_this.name, "' blocked by other connection holding version ").concat(ev.oldVersion / 10)); + }); + this._maxKey = getMaxKey(options.IDBKeyRange); + this._createTransaction = function (mode, storeNames, dbschema, parentTransaction) { return new _this.Transaction(mode, storeNames, dbschema, _this._options.chromeTransactionDurability, parentTransaction); }; + this._fireOnBlocked = function (ev) { + _this.on("blocked").fire(ev); + connections + .filter(function (c) { return c.name === _this.name && c !== _this && !c._state.vcFired; }) + .map(function (c) { return c.on("versionchange").fire(ev); }); + }; + this.use(cacheExistingValuesMiddleware); + this.use(cacheMiddleware); + this.use(observabilityMiddleware); + this.use(virtualIndexMiddleware); + this.use(hooksMiddleware); + var vipDB = new Proxy(this, { + get: function (_, prop, receiver) { + if (prop === '_vip') + return true; + if (prop === 'table') + return function (tableName) { return vipify(_this.table(tableName), vipDB); }; + var rv = Reflect.get(_, prop, receiver); + if (rv instanceof Table) + return vipify(rv, vipDB); + if (prop === 'tables') + return rv.map(function (t) { return vipify(t, vipDB); }); + if (prop === '_createTransaction') + return function () { + var tx = rv.apply(this, arguments); + return vipify(tx, vipDB); + }; + return rv; + } + }); + this.vip = vipDB; + addons.forEach(function (addon) { return addon(_this); }); + } + Dexie.prototype.version = function (versionNumber) { + if (isNaN(versionNumber) || versionNumber < 0.1) + throw new exceptions.Type("Given version is not a positive number"); + versionNumber = Math.round(versionNumber * 10) / 10; + if (this.idbdb || this._state.isBeingOpened) + throw new exceptions.Schema("Cannot add version when database is open"); + this.verno = Math.max(this.verno, versionNumber); + var versions = this._versions; + var versionInstance = versions.filter(function (v) { return v._cfg.version === versionNumber; })[0]; + if (versionInstance) + return versionInstance; + versionInstance = new this.Version(versionNumber); + versions.push(versionInstance); + versions.sort(lowerVersionFirst); + versionInstance.stores({}); + this._state.autoSchema = false; + return versionInstance; + }; + Dexie.prototype._whenReady = function (fn) { + var _this = this; + return (this.idbdb && (this._state.openComplete || PSD.letThrough || this._vip)) ? fn() : new DexiePromise(function (resolve, reject) { + if (_this._state.openComplete) { + return reject(new exceptions.DatabaseClosed(_this._state.dbOpenError)); + } + if (!_this._state.isBeingOpened) { + if (!_this._state.autoOpen) { + reject(new exceptions.DatabaseClosed()); + return; + } + _this.open().catch(nop); + } + _this._state.dbReadyPromise.then(resolve, reject); + }).then(fn); + }; + Dexie.prototype.use = function (_a) { + var stack = _a.stack, create = _a.create, level = _a.level, name = _a.name; + if (name) + this.unuse({ stack: stack, name: name }); + var middlewares = this._middlewares[stack] || (this._middlewares[stack] = []); + middlewares.push({ stack: stack, create: create, level: level == null ? 10 : level, name: name }); + middlewares.sort(function (a, b) { return a.level - b.level; }); + return this; + }; + Dexie.prototype.unuse = function (_a) { + var stack = _a.stack, name = _a.name, create = _a.create; + if (stack && this._middlewares[stack]) { + this._middlewares[stack] = this._middlewares[stack].filter(function (mw) { + return create ? mw.create !== create : + name ? mw.name !== name : + false; + }); + } + return this; + }; + Dexie.prototype.open = function () { + var _this = this; + return usePSD(globalPSD, + function () { return dexieOpen(_this); }); + }; + Dexie.prototype._close = function () { + var state = this._state; + var idx = connections.indexOf(this); + if (idx >= 0) + connections.splice(idx, 1); + if (this.idbdb) { + try { + this.idbdb.close(); + } + catch (e) { } + this.idbdb = null; + } + if (!state.isBeingOpened) { + state.dbReadyPromise = new DexiePromise(function (resolve) { + state.dbReadyResolve = resolve; + }); + state.openCanceller = new DexiePromise(function (_, reject) { + state.cancelOpen = reject; + }); + } + }; + Dexie.prototype.close = function (_a) { + var _b = _a === void 0 ? { disableAutoOpen: true } : _a, disableAutoOpen = _b.disableAutoOpen; + var state = this._state; + if (disableAutoOpen) { + if (state.isBeingOpened) { + state.cancelOpen(new exceptions.DatabaseClosed()); + } + this._close(); + state.autoOpen = false; + state.dbOpenError = new exceptions.DatabaseClosed(); + } + else { + this._close(); + state.autoOpen = this._options.autoOpen || + state.isBeingOpened; + state.openComplete = false; + state.dbOpenError = null; + } + }; + Dexie.prototype.delete = function (closeOptions) { + var _this = this; + if (closeOptions === void 0) { closeOptions = { disableAutoOpen: true }; } + var hasInvalidArguments = arguments.length > 0 && typeof arguments[0] !== 'object'; + var state = this._state; + return new DexiePromise(function (resolve, reject) { + var doDelete = function () { + _this.close(closeOptions); + var req = _this._deps.indexedDB.deleteDatabase(_this.name); + req.onsuccess = wrap(function () { + _onDatabaseDeleted(_this._deps, _this.name); + resolve(); + }); + req.onerror = eventRejectHandler(reject); + req.onblocked = _this._fireOnBlocked; + }; + if (hasInvalidArguments) + throw new exceptions.InvalidArgument("Invalid closeOptions argument to db.delete()"); + if (state.isBeingOpened) { + state.dbReadyPromise.then(doDelete); + } + else { + doDelete(); + } + }); + }; + Dexie.prototype.backendDB = function () { + return this.idbdb; + }; + Dexie.prototype.isOpen = function () { + return this.idbdb !== null; + }; + Dexie.prototype.hasBeenClosed = function () { + var dbOpenError = this._state.dbOpenError; + return dbOpenError && (dbOpenError.name === 'DatabaseClosed'); + }; + Dexie.prototype.hasFailed = function () { + return this._state.dbOpenError !== null; + }; + Dexie.prototype.dynamicallyOpened = function () { + return this._state.autoSchema; + }; + Object.defineProperty(Dexie.prototype, "tables", { + get: function () { + var _this = this; + return keys(this._allTables).map(function (name) { return _this._allTables[name]; }); + }, + enumerable: false, + configurable: true + }); + Dexie.prototype.transaction = function () { + var args = extractTransactionArgs.apply(this, arguments); + return this._transaction.apply(this, args); + }; + Dexie.prototype._transaction = function (mode, tables, scopeFunc) { + var _this = this; + var parentTransaction = PSD.trans; + if (!parentTransaction || parentTransaction.db !== this || mode.indexOf('!') !== -1) + parentTransaction = null; + var onlyIfCompatible = mode.indexOf('?') !== -1; + mode = mode.replace('!', '').replace('?', ''); + var idbMode, storeNames; + try { + storeNames = tables.map(function (table) { + var storeName = table instanceof _this.Table ? table.name : table; + if (typeof storeName !== 'string') + throw new TypeError("Invalid table argument to Dexie.transaction(). Only Table or String are allowed"); + return storeName; + }); + if (mode == "r" || mode === READONLY) + idbMode = READONLY; + else if (mode == "rw" || mode == READWRITE) + idbMode = READWRITE; + else + throw new exceptions.InvalidArgument("Invalid transaction mode: " + mode); + if (parentTransaction) { + if (parentTransaction.mode === READONLY && idbMode === READWRITE) { + if (onlyIfCompatible) { + parentTransaction = null; + } + else + throw new exceptions.SubTransaction("Cannot enter a sub-transaction with READWRITE mode when parent transaction is READONLY"); + } + if (parentTransaction) { + storeNames.forEach(function (storeName) { + if (parentTransaction && parentTransaction.storeNames.indexOf(storeName) === -1) { + if (onlyIfCompatible) { + parentTransaction = null; + } + else + throw new exceptions.SubTransaction("Table " + storeName + + " not included in parent transaction."); + } + }); + } + if (onlyIfCompatible && parentTransaction && !parentTransaction.active) { + parentTransaction = null; + } + } + } + catch (e) { + return parentTransaction ? + parentTransaction._promise(null, function (_, reject) { reject(e); }) : + rejection(e); + } + var enterTransaction = enterTransactionScope.bind(null, this, idbMode, storeNames, parentTransaction, scopeFunc); + return (parentTransaction ? + parentTransaction._promise(idbMode, enterTransaction, "lock") : + PSD.trans ? + usePSD(PSD.transless, function () { return _this._whenReady(enterTransaction); }) : + this._whenReady(enterTransaction)); + }; + Dexie.prototype.table = function (tableName) { + if (!hasOwn(this._allTables, tableName)) { + throw new exceptions.InvalidTable("Table ".concat(tableName, " does not exist")); + } + return this._allTables[tableName]; + }; + return Dexie; +}()); + +var symbolObservable = typeof Symbol !== "undefined" && "observable" in Symbol + ? Symbol.observable + : "@@observable"; +var Observable = (function () { + function Observable(subscribe) { + this._subscribe = subscribe; + } + Observable.prototype.subscribe = function (x, error, complete) { + return this._subscribe(!x || typeof x === "function" ? { next: x, error: error, complete: complete } : x); + }; + Observable.prototype[symbolObservable] = function () { + return this; + }; + return Observable; +}()); + +var domDeps; +try { + domDeps = { + indexedDB: _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.msIndexedDB, + IDBKeyRange: _global.IDBKeyRange || _global.webkitIDBKeyRange + }; +} +catch (e) { + domDeps = { indexedDB: null, IDBKeyRange: null }; +} + +function liveQuery(querier) { + var hasValue = false; + var currentValue; + var observable = new Observable(function (observer) { + var scopeFuncIsAsync = isAsyncFunction(querier); + function execute(ctx) { + var wasRootExec = beginMicroTickScope(); + try { + if (scopeFuncIsAsync) { + incrementExpectedAwaits(); + } + var rv = newScope(querier, ctx); + if (scopeFuncIsAsync) { + rv = rv.finally(decrementExpectedAwaits); + } + return rv; + } + finally { + wasRootExec && endMicroTickScope(); + } + } + var closed = false; + var abortController; + var accumMuts = {}; + var currentObs = {}; + var subscription = { + get closed() { + return closed; + }, + unsubscribe: function () { + if (closed) + return; + closed = true; + if (abortController) + abortController.abort(); + if (startedListening) + globalEvents.storagemutated.unsubscribe(mutationListener); + }, + }; + observer.start && observer.start(subscription); + var startedListening = false; + var doQuery = function () { return execInGlobalContext(_doQuery); }; + function shouldNotify() { + return obsSetsOverlap(currentObs, accumMuts); + } + var mutationListener = function (parts) { + extendObservabilitySet(accumMuts, parts); + if (shouldNotify()) { + doQuery(); + } + }; + var _doQuery = function () { + if (closed || + !domDeps.indexedDB) + { + return; + } + accumMuts = {}; + var subscr = {}; + if (abortController) + abortController.abort(); + abortController = new AbortController(); + var ctx = { + subscr: subscr, + signal: abortController.signal, + requery: doQuery, + querier: querier, + trans: null + }; + var ret = execute(ctx); + Promise.resolve(ret).then(function (result) { + hasValue = true; + currentValue = result; + if (closed || ctx.signal.aborted) { + return; + } + accumMuts = {}; + currentObs = subscr; + if (!objectIsEmpty(currentObs) && !startedListening) { + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, mutationListener); + startedListening = true; + } + execInGlobalContext(function () { return !closed && observer.next && observer.next(result); }); + }, function (err) { + hasValue = false; + if (!['DatabaseClosedError', 'AbortError'].includes(err === null || err === void 0 ? void 0 : err.name)) { + if (!closed) + execInGlobalContext(function () { + if (closed) + return; + observer.error && observer.error(err); + }); + } + }); + }; + setTimeout(doQuery, 0); + return subscription; + }); + observable.hasValue = function () { return hasValue; }; + observable.getValue = function () { return currentValue; }; + return observable; +} + +var Dexie = Dexie$1; +props(Dexie, __assign(__assign({}, fullNameExceptions), { + delete: function (databaseName) { + var db = new Dexie(databaseName, { addons: [] }); + return db.delete(); + }, + exists: function (name) { + return new Dexie(name, { addons: [] }).open().then(function (db) { + db.close(); + return true; + }).catch('NoSuchDatabaseError', function () { return false; }); + }, + getDatabaseNames: function (cb) { + try { + return getDatabaseNames(Dexie.dependencies).then(cb); + } + catch (_a) { + return rejection(new exceptions.MissingAPI()); + } + }, + defineClass: function () { + function Class(content) { + extend(this, content); + } + return Class; + }, ignoreTransaction: function (scopeFunc) { + return PSD.trans ? + usePSD(PSD.transless, scopeFunc) : + scopeFunc(); + }, vip: vip, async: function (generatorFn) { + return function () { + try { + var rv = awaitIterator(generatorFn.apply(this, arguments)); + if (!rv || typeof rv.then !== 'function') + return DexiePromise.resolve(rv); + return rv; + } + catch (e) { + return rejection(e); + } + }; + }, spawn: function (generatorFn, args, thiz) { + try { + var rv = awaitIterator(generatorFn.apply(thiz, args || [])); + if (!rv || typeof rv.then !== 'function') + return DexiePromise.resolve(rv); + return rv; + } + catch (e) { + return rejection(e); + } + }, + currentTransaction: { + get: function () { return PSD.trans || null; } + }, waitFor: function (promiseOrFunction, optionalTimeout) { + var promise = DexiePromise.resolve(typeof promiseOrFunction === 'function' ? + Dexie.ignoreTransaction(promiseOrFunction) : + promiseOrFunction) + .timeout(optionalTimeout || 60000); + return PSD.trans ? + PSD.trans.waitFor(promise) : + promise; + }, + Promise: DexiePromise, + debug: { + get: function () { return debug; }, + set: function (value) { + setDebug(value); + } + }, + derive: derive, extend: extend, props: props, override: override, + Events: Events, on: globalEvents, liveQuery: liveQuery, extendObservabilitySet: extendObservabilitySet, + getByKeyPath: getByKeyPath, setByKeyPath: setByKeyPath, delByKeyPath: delByKeyPath, shallowClone: shallowClone, deepClone: deepClone, getObjectDiff: getObjectDiff, cmp: cmp, asap: asap$1, + minKey: minKey, + addons: [], + connections: connections, + errnames: errnames, + dependencies: domDeps, cache: cache, + semVer: DEXIE_VERSION, version: DEXIE_VERSION.split('.') + .map(function (n) { return parseInt(n); }) + .reduce(function (p, c, i) { return p + (c / Math.pow(10, i * 2)); }) })); +Dexie.maxKey = getMaxKey(Dexie.dependencies.IDBKeyRange); + +if (typeof dispatchEvent !== 'undefined' && typeof addEventListener !== 'undefined') { + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function (updatedParts) { + if (!propagatingLocally) { + var event_1; + event_1 = new CustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, { + detail: updatedParts + }); + propagatingLocally = true; + dispatchEvent(event_1); + propagatingLocally = false; + } + }); + addEventListener(STORAGE_MUTATED_DOM_EVENT_NAME, function (_a) { + var detail = _a.detail; + if (!propagatingLocally) { + propagateLocally(detail); + } + }); +} +function propagateLocally(updateParts) { + var wasMe = propagatingLocally; + try { + propagatingLocally = true; + globalEvents.storagemutated.fire(updateParts); + signalSubscribersNow(updateParts, true); + } + finally { + propagatingLocally = wasMe; + } +} +var propagatingLocally = false; + +var bc; +var createBC = function () { }; +if (typeof BroadcastChannel !== 'undefined') { + createBC = function () { + bc = new BroadcastChannel(STORAGE_MUTATED_DOM_EVENT_NAME); + bc.onmessage = function (ev) { return ev.data && propagateLocally(ev.data); }; + }; + createBC(); + if (typeof bc.unref === 'function') { + bc.unref(); + } + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function (changedParts) { + if (!propagatingLocally) { + bc.postMessage(changedParts); + } + }); +} + +if (typeof addEventListener !== 'undefined') { + addEventListener('pagehide', function (event) { + if (!Dexie$1.disableBfCache && event.persisted) { + if (debug) + console.debug('Dexie: handling persisted pagehide'); + bc === null || bc === void 0 ? void 0 : bc.close(); + for (var _i = 0, connections_1 = connections; _i < connections_1.length; _i++) { + var db = connections_1[_i]; + db.close({ disableAutoOpen: false }); + } + } + }); + addEventListener('pageshow', function (event) { + if (!Dexie$1.disableBfCache && event.persisted) { + if (debug) + console.debug('Dexie: handling persisted pageshow'); + createBC(); + propagateLocally({ all: new RangeSet(-Infinity, [[]]) }); + } + }); +} + +function add(value) { + return new PropModification({ add: value }); +} + +function remove(value) { + return new PropModification({ remove: value }); +} + +function replacePrefix(a, b) { + return new PropModification({ replacePrefix: [a, b] }); +} + +DexiePromise.rejectionMapper = mapError; +setDebug(debug); + +export { Dexie$1 as Dexie, Entity, PropModSymbol, PropModification, RangeSet, add, cmp, Dexie$1 as default, liveQuery, mergeRanges, rangesOverlap, remove, replacePrefix }; +//# sourceMappingURL=dexie.mjs.map diff --git a/libs/fflate.mjs b/libs/fflate.mjs new file mode 100644 index 0000000..e1a21e9 --- /dev/null +++ b/libs/fflate.mjs @@ -0,0 +1,2665 @@ +// DEFLATE is a complex format; to read this code, you should probably check the RFC first: +// https://tools.ietf.org/html/rfc1951 +// You may also wish to take a look at the guide I made about this program: +// https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad +// Some of the following code is similar to that of UZIP.js: +// https://github.com/photopea/UZIP.js +// However, the vast majority of the codebase has diverged from UZIP.js to increase performance and reduce bundle size. +// Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint +// is better for memory in most engines (I *think*). +var ch2 = {}; +var wk = (function (c, id, msg, transfer, cb) { + var w = new Worker(ch2[id] || (ch2[id] = URL.createObjectURL(new Blob([ + c + ';addEventListener("error",function(e){e=e.error;postMessage({$e$:[e.message,e.code,e.stack]})})' + ], { type: 'text/javascript' })))); + w.onmessage = function (e) { + var d = e.data, ed = d.$e$; + if (ed) { + var err = new Error(ed[0]); + err['code'] = ed[1]; + err.stack = ed[2]; + cb(err, null); + } + else + cb(null, d); + }; + w.postMessage(msg, transfer); + return w; +}); + +// aliases for shorter compressed code (most minifers don't do this) +var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array; +// fixed length extra bits +var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]); +// fixed distance extra bits +var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]); +// code length index map +var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); +// get base, reverse index map from extra bits +var freb = function (eb, start) { + var b = new u16(31); + for (var i = 0; i < 31; ++i) { + b[i] = start += 1 << eb[i - 1]; + } + // numbers here are at max 18 bits + var r = new i32(b[30]); + for (var i = 1; i < 30; ++i) { + for (var j = b[i]; j < b[i + 1]; ++j) { + r[j] = ((j - b[i]) << 5) | i; + } + } + return { b: b, r: r }; +}; +var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r; +// we can ignore the fact that the other numbers are wrong; they never happen anyway +fl[28] = 258, revfl[258] = 28; +var _b = freb(fdeb, 0), fd = _b.b, revfd = _b.r; +// map of value to reverse (assuming 16 bits) +var rev = new u16(32768); +for (var i = 0; i < 32768; ++i) { + // reverse table algorithm from SO + var x = ((i & 0xAAAA) >> 1) | ((i & 0x5555) << 1); + x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2); + x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4); + rev[i] = (((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8)) >> 1; +} +// create huffman tree from u8 "map": index -> code length for code index +// mb (max bits) must be at most 15 +// TODO: optimize/split up? +var hMap = (function (cd, mb, r) { + var s = cd.length; + // index + var i = 0; + // u16 "map": index -> # of codes with bit length = index + var l = new u16(mb); + // length of cd must be 288 (total # of codes) + for (; i < s; ++i) { + if (cd[i]) + ++l[cd[i] - 1]; + } + // u16 "map": index -> minimum code for bit length = index + var le = new u16(mb); + for (i = 1; i < mb; ++i) { + le[i] = (le[i - 1] + l[i - 1]) << 1; + } + var co; + if (r) { + // u16 "map": index -> number of actual bits, symbol for code + co = new u16(1 << mb); + // bits to remove for reverser + var rvb = 15 - mb; + for (i = 0; i < s; ++i) { + // ignore 0 lengths + if (cd[i]) { + // num encoding both symbol and bits read + var sv = (i << 4) | cd[i]; + // free bits + var r_1 = mb - cd[i]; + // start value + var v = le[cd[i] - 1]++ << r_1; + // m is end value + for (var m = v | ((1 << r_1) - 1); v <= m; ++v) { + // every 16 bit value starting with the code yields the same result + co[rev[v] >> rvb] = sv; + } + } + } + } + else { + co = new u16(s); + for (i = 0; i < s; ++i) { + if (cd[i]) { + co[i] = rev[le[cd[i] - 1]++] >> (15 - cd[i]); + } + } + } + return co; +}); +// fixed length tree +var flt = new u8(288); +for (var i = 0; i < 144; ++i) + flt[i] = 8; +for (var i = 144; i < 256; ++i) + flt[i] = 9; +for (var i = 256; i < 280; ++i) + flt[i] = 7; +for (var i = 280; i < 288; ++i) + flt[i] = 8; +// fixed distance tree +var fdt = new u8(32); +for (var i = 0; i < 32; ++i) + fdt[i] = 5; +// fixed length map +var flm = /*#__PURE__*/ hMap(flt, 9, 0), flrm = /*#__PURE__*/ hMap(flt, 9, 1); +// fixed distance map +var fdm = /*#__PURE__*/ hMap(fdt, 5, 0), fdrm = /*#__PURE__*/ hMap(fdt, 5, 1); +// find max of array +var max = function (a) { + var m = a[0]; + for (var i = 1; i < a.length; ++i) { + if (a[i] > m) + m = a[i]; + } + return m; +}; +// read d, starting at bit p and mask with m +var bits = function (d, p, m) { + var o = (p / 8) | 0; + return ((d[o] | (d[o + 1] << 8)) >> (p & 7)) & m; +}; +// read d, starting at bit p continuing for at least 16 bits +var bits16 = function (d, p) { + var o = (p / 8) | 0; + return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >> (p & 7)); +}; +// get end of byte +var shft = function (p) { return ((p + 7) / 8) | 0; }; +// typed array slice - allows garbage collector to free original reference, +// while being more compatible than .slice +var slc = function (v, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v.length) + e = v.length; + // can't use .constructor in case user-supplied + return new u8(v.subarray(s, e)); +}; +/** + * Codes for errors generated within this library + */ +export var FlateErrorCode = { + UnexpectedEOF: 0, + InvalidBlockType: 1, + InvalidLengthLiteral: 2, + InvalidDistance: 3, + StreamFinished: 4, + NoStreamHandler: 5, + InvalidHeader: 6, + NoCallback: 7, + InvalidUTF8: 8, + ExtraFieldTooLong: 9, + InvalidDate: 10, + FilenameTooLong: 11, + StreamFinishing: 12, + InvalidZipData: 13, + UnknownCompressionMethod: 14 +}; +// error codes +var ec = [ + 'unexpected EOF', + 'invalid block type', + 'invalid length/literal', + 'invalid distance', + 'stream finished', + 'no stream handler', + , + 'no callback', + 'invalid UTF-8 data', + 'extra field too long', + 'date not in range 1980-2099', + 'filename too long', + 'stream finishing', + 'invalid zip data' + // determined by unknown compression method +]; +; +var err = function (ind, msg, nt) { + var e = new Error(msg || ec[ind]); + e.code = ind; + if (Error.captureStackTrace) + Error.captureStackTrace(e, err); + if (!nt) + throw e; + return e; +}; +// expands raw DEFLATE data +var inflt = function (dat, st, buf, dict) { + // source length dict length + var sl = dat.length, dl = dict ? dict.length : 0; + if (!sl || st.f && !st.l) + return buf || new u8(0); + var noBuf = !buf; + // have to estimate size + var resize = noBuf || st.i != 2; + // no state + var noSt = st.i; + // Assumes roughly 33% compression ratio average + if (noBuf) + buf = new u8(sl * 3); + // ensure buffer can fit at least l elements + var cbuf = function (l) { + var bl = buf.length; + // need to increase size to fit + if (l > bl) { + // Double or set to necessary, whichever is greater + var nbuf = new u8(Math.max(bl * 2, l)); + nbuf.set(buf); + buf = nbuf; + } + }; + // last chunk bitpos bytes + var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; + // total bits + var tbts = sl * 8; + do { + if (!lm) { + // BFINAL - this is only 1 when last chunk is next + final = bits(dat, pos, 1); + // type: 0 = no compression, 1 = fixed huffman, 2 = dynamic huffman + var type = bits(dat, pos + 1, 3); + pos += 3; + if (!type) { + // go to end of byte boundary + var s = shft(pos) + 4, l = dat[s - 4] | (dat[s - 3] << 8), t = s + l; + if (t > sl) { + if (noSt) + err(0); + break; + } + // ensure size + if (resize) + cbuf(bt + l); + // Copy over uncompressed data + buf.set(dat.subarray(s, t), bt); + // Get new bitpos, update byte count + st.b = bt += l, st.p = pos = t * 8, st.f = final; + continue; + } + else if (type == 1) + lm = flrm, dm = fdrm, lbt = 9, dbt = 5; + else if (type == 2) { + // literal lengths + var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + var tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + // length+distance tree + var ldt = new u8(tl); + // code length tree + var clt = new u8(19); + for (var i = 0; i < hcLen; ++i) { + // use index map to get real code + clt[clim[i]] = bits(dat, pos + i * 3, 7); + } + pos += hcLen * 3; + // code lengths bits + var clb = max(clt), clbmsk = (1 << clb) - 1; + // code lengths map + var clm = hMap(clt, clb, 1); + for (var i = 0; i < tl;) { + var r = clm[bits(dat, pos, clbmsk)]; + // bits read + pos += r & 15; + // symbol + var s = r >> 4; + // code length to copy + if (s < 16) { + ldt[i++] = s; + } + else { + // copy count + var c = 0, n = 0; + if (s == 16) + n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; + else if (s == 17) + n = 3 + bits(dat, pos, 7), pos += 3; + else if (s == 18) + n = 11 + bits(dat, pos, 127), pos += 7; + while (n--) + ldt[i++] = c; + } + } + // length tree distance tree + var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + // max length bits + lbt = max(lt); + // max dist bits + dbt = max(dt); + lm = hMap(lt, lbt, 1); + dm = hMap(dt, dbt, 1); + } + else + err(1); + if (pos > tbts) { + if (noSt) + err(0); + break; + } + } + // Make sure the buffer can hold this + the largest possible addition + // Maximum chunk size (practically, theoretically infinite) is 2^17 + if (resize) + cbuf(bt + 131072); + var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + var lpos = pos; + for (;; lpos = pos) { + // bits read, code + var c = lm[bits16(dat, pos) & lms], sym = c >> 4; + pos += c & 15; + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (!c) + err(2); + if (sym < 256) + buf[bt++] = sym; + else if (sym == 256) { + lpos = pos, lm = null; + break; + } + else { + var add = sym - 254; + // no extra bits needed if less + if (sym > 264) { + // index + var i = sym - 257, b = fleb[i]; + add = bits(dat, pos, (1 << b) - 1) + fl[i]; + pos += b; + } + // dist + var d = dm[bits16(dat, pos) & dms], dsym = d >> 4; + if (!d) + err(3); + pos += d & 15; + var dt = fd[dsym]; + if (dsym > 3) { + var b = fdeb[dsym]; + dt += bits16(dat, pos) & (1 << b) - 1, pos += b; + } + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + 131072); + var end = bt + add; + if (bt < dt) { + var shift = dl - dt, dend = Math.min(dt, end); + if (shift + bt < 0) + err(3); + for (; bt < dend; ++bt) + buf[bt] = dict[shift + bt]; + } + for (; bt < end; ++bt) + buf[bt] = buf[bt - dt]; + } + } + st.l = lm, st.p = lpos, st.b = bt, st.f = final; + if (lm) + final = 1, st.m = lbt, st.d = dm, st.n = dbt; + } while (!final); + // don't reallocate for streams or user buffers + return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt); +}; +// starting at p, write the minimum number of bits that can hold v to d +var wbits = function (d, p, v) { + v <<= p & 7; + var o = (p / 8) | 0; + d[o] |= v; + d[o + 1] |= v >> 8; +}; +// starting at p, write the minimum number of bits (>8) that can hold v to d +var wbits16 = function (d, p, v) { + v <<= p & 7; + var o = (p / 8) | 0; + d[o] |= v; + d[o + 1] |= v >> 8; + d[o + 2] |= v >> 16; +}; +// creates code lengths from a frequency table +var hTree = function (d, mb) { + // Need extra info to make a tree + var t = []; + for (var i = 0; i < d.length; ++i) { + if (d[i]) + t.push({ s: i, f: d[i] }); + } + var s = t.length; + var t2 = t.slice(); + if (!s) + return { t: et, l: 0 }; + if (s == 1) { + var v = new u8(t[0].s + 1); + v[t[0].s] = 1; + return { t: v, l: 1 }; + } + t.sort(function (a, b) { return a.f - b.f; }); + // after i2 reaches last ind, will be stopped + // freq must be greater than largest possible number of symbols + t.push({ s: -1, f: 25001 }); + var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2; + t[0] = { s: -1, f: l.f + r.f, l: l, r: r }; + // efficient algorithm from UZIP.js + // i0 is lookbehind, i2 is lookahead - after processing two low-freq + // symbols that combined have high freq, will start processing i2 (high-freq, + // non-composite) symbols instead + // see https://reddit.com/r/photopea/comments/ikekht/uzipjs_questions/ + while (i1 != s - 1) { + l = t[t[i0].f < t[i2].f ? i0++ : i2++]; + r = t[i0 != i1 && t[i0].f < t[i2].f ? i0++ : i2++]; + t[i1++] = { s: -1, f: l.f + r.f, l: l, r: r }; + } + var maxSym = t2[0].s; + for (var i = 1; i < s; ++i) { + if (t2[i].s > maxSym) + maxSym = t2[i].s; + } + // code lengths + var tr = new u16(maxSym + 1); + // max bits in tree + var mbt = ln(t[i1 - 1], tr, 0); + if (mbt > mb) { + // more algorithms from UZIP.js + // TODO: find out how this code works (debt) + // ind debt + var i = 0, dt = 0; + // left cost + var lft = mbt - mb, cst = 1 << lft; + t2.sort(function (a, b) { return tr[b.s] - tr[a.s] || a.f - b.f; }); + for (; i < s; ++i) { + var i2_1 = t2[i].s; + if (tr[i2_1] > mb) { + dt += cst - (1 << (mbt - tr[i2_1])); + tr[i2_1] = mb; + } + else + break; + } + dt >>= lft; + while (dt > 0) { + var i2_2 = t2[i].s; + if (tr[i2_2] < mb) + dt -= 1 << (mb - tr[i2_2]++ - 1); + else + ++i; + } + for (; i >= 0 && dt; --i) { + var i2_3 = t2[i].s; + if (tr[i2_3] == mb) { + --tr[i2_3]; + ++dt; + } + } + mbt = mb; + } + return { t: new u8(tr), l: mbt }; +}; +// get the max length and assign length codes +var ln = function (n, l, d) { + return n.s == -1 + ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) + : (l[n.s] = d); +}; +// length codes generation +var lc = function (c) { + var s = c.length; + // Note that the semicolon was intentional + while (s && !c[--s]) + ; + var cl = new u16(++s); + // ind num streak + var cli = 0, cln = c[0], cls = 1; + var w = function (v) { cl[cli++] = v; }; + for (var i = 1; i <= s; ++i) { + if (c[i] == cln && i != s) + ++cls; + else { + if (!cln && cls > 2) { + for (; cls > 138; cls -= 138) + w(32754); + if (cls > 2) { + w(cls > 10 ? ((cls - 11) << 5) | 28690 : ((cls - 3) << 5) | 12305); + cls = 0; + } + } + else if (cls > 3) { + w(cln), --cls; + for (; cls > 6; cls -= 6) + w(8304); + if (cls > 2) + w(((cls - 3) << 5) | 8208), cls = 0; + } + while (cls--) + w(cln); + cls = 1; + cln = c[i]; + } + } + return { c: cl.subarray(0, cli), n: s }; +}; +// calculate the length of output from tree, code lengths +var clen = function (cf, cl) { + var l = 0; + for (var i = 0; i < cl.length; ++i) + l += cf[i] * cl[i]; + return l; +}; +// writes a fixed block +// returns the new bit pos +var wfblk = function (out, pos, dat) { + // no need to write 00 as type: TypedArray defaults to 0 + var s = dat.length; + var o = shft(pos + 2); + out[o] = s & 255; + out[o + 1] = s >> 8; + out[o + 2] = out[o] ^ 255; + out[o + 3] = out[o + 1] ^ 255; + for (var i = 0; i < s; ++i) + out[o + i + 4] = dat[i]; + return (o + 4 + s) * 8; +}; +// writes a block +var wblk = function (dat, out, final, syms, lf, df, eb, li, bs, bl, p) { + wbits(out, p++, final); + ++lf[256]; + var _a = hTree(lf, 15), dlt = _a.t, mlb = _a.l; + var _b = hTree(df, 15), ddt = _b.t, mdb = _b.l; + var _c = lc(dlt), lclt = _c.c, nlc = _c.n; + var _d = lc(ddt), lcdt = _d.c, ndc = _d.n; + var lcfreq = new u16(19); + for (var i = 0; i < lclt.length; ++i) + ++lcfreq[lclt[i] & 31]; + for (var i = 0; i < lcdt.length; ++i) + ++lcfreq[lcdt[i] & 31]; + var _e = hTree(lcfreq, 7), lct = _e.t, mlcb = _e.l; + var nlcc = 19; + for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc) + ; + var flen = (bl + 5) << 3; + var ftlen = clen(lf, flt) + clen(df, fdt) + eb; + var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + 2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]; + if (bs >= 0 && flen <= ftlen && flen <= dtlen) + return wfblk(out, p, dat.subarray(bs, bs + bl)); + var lm, ll, dm, dl; + wbits(out, p, 1 + (dtlen < ftlen)), p += 2; + if (dtlen < ftlen) { + lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt; + var llm = hMap(lct, mlcb, 0); + wbits(out, p, nlc - 257); + wbits(out, p + 5, ndc - 1); + wbits(out, p + 10, nlcc - 4); + p += 14; + for (var i = 0; i < nlcc; ++i) + wbits(out, p + 3 * i, lct[clim[i]]); + p += 3 * nlcc; + var lcts = [lclt, lcdt]; + for (var it = 0; it < 2; ++it) { + var clct = lcts[it]; + for (var i = 0; i < clct.length; ++i) { + var len = clct[i] & 31; + wbits(out, p, llm[len]), p += lct[len]; + if (len > 15) + wbits(out, p, (clct[i] >> 5) & 127), p += clct[i] >> 12; + } + } + } + else { + lm = flm, ll = flt, dm = fdm, dl = fdt; + } + for (var i = 0; i < li; ++i) { + var sym = syms[i]; + if (sym > 255) { + var len = (sym >> 18) & 31; + wbits16(out, p, lm[len + 257]), p += ll[len + 257]; + if (len > 7) + wbits(out, p, (sym >> 23) & 31), p += fleb[len]; + var dst = sym & 31; + wbits16(out, p, dm[dst]), p += dl[dst]; + if (dst > 3) + wbits16(out, p, (sym >> 5) & 8191), p += fdeb[dst]; + } + else { + wbits16(out, p, lm[sym]), p += ll[sym]; + } + } + wbits16(out, p, lm[256]); + return p + ll[256]; +}; +// deflate options (nice << 13) | chain +var deo = /*#__PURE__*/ new i32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]); +// empty +var et = /*#__PURE__*/ new u8(0); +// compresses data into a raw DEFLATE buffer +var dflt = function (dat, lvl, plvl, pre, post, st) { + var s = st.z || dat.length; + var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7000)) + post); + // writing to this writes to the output buffer + var w = o.subarray(pre, o.length - post); + var lst = st.l; + var pos = (st.r || 0) & 7; + if (lvl) { + if (pos) + w[0] = st.r >> 3; + var opt = deo[lvl - 1]; + var n = opt >> 13, c = opt & 8191; + var msk_1 = (1 << plvl) - 1; + // prev 2-byte val map curr 2-byte val map + var prev = st.p || new u16(32768), head = st.h || new u16(msk_1 + 1); + var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1; + var hsh = function (i) { return (dat[i] ^ (dat[i + 1] << bs1_1) ^ (dat[i + 2] << bs2_1)) & msk_1; }; + // 24576 is an arbitrary number of maximum symbols per block + // 424 buffer for last block + var syms = new i32(25000); + // length/literal freq distance freq + var lf = new u16(288), df = new u16(32); + // l/lcnt exbits index l/lind waitdx blkpos + var lc_1 = 0, eb = 0, i = st.i || 0, li = 0, wi = st.w || 0, bs = 0; + for (; i + 2 < s; ++i) { + // hash value + var hv = hsh(i); + // index mod 32768 previous index mod + var imod = i & 32767, pimod = head[hv]; + prev[imod] = pimod; + head[hv] = imod; + // We always should modify head and prev, but only add symbols if + // this data is not yet processed ("wait" for wait index) + if (wi <= i) { + // bytes remaining + var rem = s - i; + if ((lc_1 > 7000 || li > 24576) && (rem > 423 || !lst)) { + pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos); + li = lc_1 = eb = 0, bs = i; + for (var j = 0; j < 286; ++j) + lf[j] = 0; + for (var j = 0; j < 30; ++j) + df[j] = 0; + } + // len dist chain + var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767; + if (rem > 2 && hv == hsh(i - dif)) { + var maxn = Math.min(n, rem) - 1; + var maxd = Math.min(32767, i); + // max possible length + // not capped at dif because decompressors implement "rolling" index population + var ml = Math.min(258, rem); + while (dif <= maxd && --ch_1 && imod != pimod) { + if (dat[i + l] == dat[i + l - dif]) { + var nl = 0; + for (; nl < ml && dat[i + nl] == dat[i + nl - dif]; ++nl) + ; + if (nl > l) { + l = nl, d = dif; + // break out early when we reach "nice" (we are satisfied enough) + if (nl > maxn) + break; + // now, find the rarest 2-byte sequence within this + // length of literals and search for that instead. + // Much faster than just using the start + var mmd = Math.min(dif, nl - 2); + var md = 0; + for (var j = 0; j < mmd; ++j) { + var ti = i - dif + j & 32767; + var pti = prev[ti]; + var cd = ti - pti & 32767; + if (cd > md) + md = cd, pimod = ti; + } + } + } + // check the previous match + imod = pimod, pimod = prev[imod]; + dif += imod - pimod & 32767; + } + } + // d will be nonzero only when a match was found + if (d) { + // store both dist and len data in one int32 + // Make sure this is recognized as a len/dist with 28th bit (2^28) + syms[li++] = 268435456 | (revfl[l] << 18) | revfd[d]; + var lin = revfl[l] & 31, din = revfd[d] & 31; + eb += fleb[lin] + fdeb[din]; + ++lf[257 + lin]; + ++df[din]; + wi = i + l; + ++lc_1; + } + else { + syms[li++] = dat[i]; + ++lf[dat[i]]; + } + } + } + for (i = Math.max(i, wi); i < s; ++i) { + syms[li++] = dat[i]; + ++lf[dat[i]]; + } + pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos); + if (!lst) { + st.r = (pos & 7) | w[(pos / 8) | 0] << 3; + // shft(pos) now 1 less if pos & 7 != 0 + pos -= 7; + st.h = head, st.p = prev, st.i = i, st.w = wi; + } + } + else { + for (var i = st.w || 0; i < s + lst; i += 65535) { + // end + var e = i + 65535; + if (e >= s) { + // write final block + w[(pos / 8) | 0] = lst; + e = s; + } + pos = wfblk(w, pos + 1, dat.subarray(i, e)); + } + st.i = s; + } + return slc(o, 0, pre + shft(pos) + post); +}; +// CRC32 table +var crct = /*#__PURE__*/ (function () { + var t = new Int32Array(256); + for (var i = 0; i < 256; ++i) { + var c = i, k = 9; + while (--k) + c = ((c & 1) && -306674912) ^ (c >>> 1); + t[i] = c; + } + return t; +})(); +// CRC32 +var crc = function () { + var c = -1; + return { + p: function (d) { + // closures have awful performance + var cr = c; + for (var i = 0; i < d.length; ++i) + cr = crct[(cr & 255) ^ d[i]] ^ (cr >>> 8); + c = cr; + }, + d: function () { return ~c; } + }; +}; +// Adler32 +var adler = function () { + var a = 1, b = 0; + return { + p: function (d) { + // closures have awful performance + var n = a, m = b; + var l = d.length | 0; + for (var i = 0; i != l;) { + var e = Math.min(i + 2655, l); + for (; i < e; ++i) + m += n += d[i]; + n = (n & 65535) + 15 * (n >> 16), m = (m & 65535) + 15 * (m >> 16); + } + a = n, b = m; + }, + d: function () { + a %= 65521, b %= 65521; + return (a & 255) << 24 | (a & 0xFF00) << 8 | (b & 255) << 8 | (b >> 8); + } + }; +}; +; +// deflate with opts +var dopt = function (dat, opt, pre, post, st) { + if (!st) { + st = { l: 1 }; + if (opt.dictionary) { + var dict = opt.dictionary.subarray(-32768); + var newDat = new u8(dict.length + dat.length); + newDat.set(dict); + newDat.set(dat, dict.length); + dat = newDat; + st.w = dict.length; + } + } + return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? (st.l ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 20) : (12 + opt.mem), pre, post, st); +}; +// Walmart object spread +var mrg = function (a, b) { + var o = {}; + for (var k in a) + o[k] = a[k]; + for (var k in b) + o[k] = b[k]; + return o; +}; +// worker clone +// This is possibly the craziest part of the entire codebase, despite how simple it may seem. +// The only parameter to this function is a closure that returns an array of variables outside of the function scope. +// We're going to try to figure out the variable names used in the closure as strings because that is crucial for workerization. +// We will return an object mapping of true variable name to value (basically, the current scope as a JS object). +// The reason we can't just use the original variable names is minifiers mangling the toplevel scope. +// This took me three weeks to figure out how to do. +var wcln = function (fn, fnStr, td) { + var dt = fn(); + var st = fn.toString(); + var ks = st.slice(st.indexOf('[') + 1, st.lastIndexOf(']')).replace(/\s+/g, '').split(','); + for (var i = 0; i < dt.length; ++i) { + var v = dt[i], k = ks[i]; + if (typeof v == 'function') { + fnStr += ';' + k + '='; + var st_1 = v.toString(); + if (v.prototype) { + // for global objects + if (st_1.indexOf('[native code]') != -1) { + var spInd = st_1.indexOf(' ', 8) + 1; + fnStr += st_1.slice(spInd, st_1.indexOf('(', spInd)); + } + else { + fnStr += st_1; + for (var t in v.prototype) + fnStr += ';' + k + '.prototype.' + t + '=' + v.prototype[t].toString(); + } + } + else + fnStr += st_1; + } + else + td[k] = v; + } + return fnStr; +}; +var ch = []; +// clone bufs +var cbfs = function (v) { + var tl = []; + for (var k in v) { + if (v[k].buffer) { + tl.push((v[k] = new v[k].constructor(v[k])).buffer); + } + } + return tl; +}; +// use a worker to execute code +var wrkr = function (fns, init, id, cb) { + if (!ch[id]) { + var fnStr = '', td_1 = {}, m = fns.length - 1; + for (var i = 0; i < m; ++i) + fnStr = wcln(fns[i], fnStr, td_1); + ch[id] = { c: wcln(fns[m], fnStr, td_1), e: td_1 }; + } + var td = mrg({}, ch[id].e); + return wk(ch[id].c + ';onmessage=function(e){for(var k in e.data)self[k]=e.data[k];onmessage=' + init.toString() + '}', id, td, cbfs(td), cb); +}; +// base async inflate fn +var bInflt = function () { return [u8, u16, i32, fleb, fdeb, clim, fl, fd, flrm, fdrm, rev, ec, hMap, max, bits, bits16, shft, slc, err, inflt, inflateSync, pbf, gopt]; }; +var bDflt = function () { return [u8, u16, i32, fleb, fdeb, clim, revfl, revfd, flm, flt, fdm, fdt, rev, deo, et, hMap, wbits, wbits16, hTree, ln, lc, clen, wfblk, wblk, shft, slc, dflt, dopt, deflateSync, pbf]; }; +// gzip extra +var gze = function () { return [gzh, gzhl, wbytes, crc, crct]; }; +// gunzip extra +var guze = function () { return [gzs, gzl]; }; +// zlib extra +var zle = function () { return [zlh, wbytes, adler]; }; +// unzlib extra +var zule = function () { return [zls]; }; +// post buf +var pbf = function (msg) { return postMessage(msg, [msg.buffer]); }; +// get opts +var gopt = function (o) { return o && { + out: o.size && new u8(o.size), + dictionary: o.dictionary +}; }; +// async helper +var cbify = function (dat, opts, fns, init, id, cb) { + var w = wrkr(fns, init, id, function (err, dat) { + w.terminate(); + cb(err, dat); + }); + w.postMessage([dat, opts], opts.consume ? [dat.buffer] : []); + return function () { w.terminate(); }; +}; +// auto stream +var astrm = function (strm) { + strm.ondata = function (dat, final) { return postMessage([dat, final], [dat.buffer]); }; + return function (ev) { + if (ev.data.length) { + strm.push(ev.data[0], ev.data[1]); + postMessage([ev.data[0].length]); + } + else + strm.flush(); + }; +}; +// async stream attach +var astrmify = function (fns, strm, opts, init, id, flush, ext) { + var t; + var w = wrkr(fns, init, id, function (err, dat) { + if (err) + w.terminate(), strm.ondata.call(strm, err); + else if (!Array.isArray(dat)) + ext(dat); + else if (dat.length == 1) { + strm.queuedSize -= dat[0]; + if (strm.ondrain) + strm.ondrain(dat[0]); + } + else { + if (dat[1]) + w.terminate(); + strm.ondata.call(strm, err, dat[0], dat[1]); + } + }); + w.postMessage(opts); + strm.queuedSize = 0; + strm.push = function (d, f) { + if (!strm.ondata) + err(5); + if (t) + strm.ondata(err(4, 0, 1), null, !!f); + strm.queuedSize += d.length; + w.postMessage([d, t = f], [d.buffer]); + }; + strm.terminate = function () { w.terminate(); }; + if (flush) { + strm.flush = function () { w.postMessage([]); }; + } +}; +// read 2 bytes +var b2 = function (d, b) { return d[b] | (d[b + 1] << 8); }; +// read 4 bytes +var b4 = function (d, b) { return (d[b] | (d[b + 1] << 8) | (d[b + 2] << 16) | (d[b + 3] << 24)) >>> 0; }; +var b8 = function (d, b) { return b4(d, b) + (b4(d, b + 4) * 4294967296); }; +// write bytes +var wbytes = function (d, b, v) { + for (; v; ++b) + d[b] = v, v >>>= 8; +}; +// gzip header +var gzh = function (c, o) { + var fn = o.filename; + c[0] = 31, c[1] = 139, c[2] = 8, c[8] = o.level < 2 ? 4 : o.level == 9 ? 2 : 0, c[9] = 3; // assume Unix + if (o.mtime != 0) + wbytes(c, 4, Math.floor(new Date(o.mtime || Date.now()) / 1000)); + if (fn) { + c[3] = 8; + for (var i = 0; i <= fn.length; ++i) + c[i + 10] = fn.charCodeAt(i); + } +}; +// gzip footer: -8 to -4 = CRC, -4 to -0 is length +// gzip start +var gzs = function (d) { + if (d[0] != 31 || d[1] != 139 || d[2] != 8) + err(6, 'invalid gzip data'); + var flg = d[3]; + var st = 10; + if (flg & 4) + st += (d[10] | d[11] << 8) + 2; + for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++]) + ; + return st + (flg & 2); +}; +// gzip length +var gzl = function (d) { + var l = d.length; + return (d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16 | d[l - 1] << 24) >>> 0; +}; +// gzip header length +var gzhl = function (o) { return 10 + (o.filename ? o.filename.length + 1 : 0); }; +// zlib header +var zlh = function (c, o) { + var lv = o.level, fl = lv == 0 ? 0 : lv < 6 ? 1 : lv == 9 ? 3 : 2; + c[0] = 120, c[1] = (fl << 6) | (o.dictionary && 32); + c[1] |= 31 - ((c[0] << 8) | c[1]) % 31; + if (o.dictionary) { + var h = adler(); + h.p(o.dictionary); + wbytes(c, 2, h.d()); + } +}; +// zlib start +var zls = function (d, dict) { + if ((d[0] & 15) != 8 || (d[0] >> 4) > 7 || ((d[0] << 8 | d[1]) % 31)) + err(6, 'invalid zlib data'); + if ((d[1] >> 5 & 1) == +!dict) + err(6, 'invalid zlib data: ' + (d[1] & 32 ? 'need' : 'unexpected') + ' dictionary'); + return (d[1] >> 3 & 4) + 2; +}; +function StrmOpt(opts, cb) { + if (typeof opts == 'function') + cb = opts, opts = {}; + this.ondata = cb; + return opts; +} +/** + * Streaming DEFLATE compression + */ +var Deflate = /*#__PURE__*/ (function () { + function Deflate(opts, cb) { + if (typeof opts == 'function') + cb = opts, opts = {}; + this.ondata = cb; + this.o = opts || {}; + this.s = { l: 0, i: 32768, w: 32768, z: 32768 }; + // Buffer length must always be 0 mod 32768 for index calculations to be correct when modifying head and prev + // 98304 = 32768 (lookback) + 65536 (common chunk size) + this.b = new u8(98304); + if (this.o.dictionary) { + var dict = this.o.dictionary.subarray(-32768); + this.b.set(dict, 32768 - dict.length); + this.s.i = 32768 - dict.length; + } + } + Deflate.prototype.p = function (c, f) { + this.ondata(dopt(c, this.o, 0, 0, this.s), f); + }; + /** + * Pushes a chunk to be deflated + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + Deflate.prototype.push = function (chunk, final) { + if (!this.ondata) + err(5); + if (this.s.l) + err(4); + var endLen = chunk.length + this.s.z; + if (endLen > this.b.length) { + if (endLen > 2 * this.b.length - 32768) { + var newBuf = new u8(endLen & -32768); + newBuf.set(this.b.subarray(0, this.s.z)); + this.b = newBuf; + } + var split = this.b.length - this.s.z; + this.b.set(chunk.subarray(0, split), this.s.z); + this.s.z = this.b.length; + this.p(this.b, false); + this.b.set(this.b.subarray(-32768)); + this.b.set(chunk.subarray(split), 32768); + this.s.z = chunk.length - split + 32768; + this.s.i = 32766, this.s.w = 32768; + } + else { + this.b.set(chunk, this.s.z); + this.s.z += chunk.length; + } + this.s.l = final & 1; + if (this.s.z > this.s.w + 8191 || final) { + this.p(this.b, final || false); + this.s.w = this.s.i, this.s.i -= 2; + } + }; + /** + * Flushes buffered uncompressed data. Useful to immediately retrieve the + * deflated output for small inputs. + */ + Deflate.prototype.flush = function () { + if (!this.ondata) + err(5); + if (this.s.l) + err(4); + this.p(this.b, false); + this.s.w = this.s.i, this.s.i -= 2; + }; + return Deflate; +}()); +export { Deflate }; +/** + * Asynchronous streaming DEFLATE compression + */ +var AsyncDeflate = /*#__PURE__*/ (function () { + function AsyncDeflate(opts, cb) { + astrmify([ + bDflt, + function () { return [astrm, Deflate]; } + ], this, StrmOpt.call(this, opts, cb), function (ev) { + var strm = new Deflate(ev.data); + onmessage = astrm(strm); + }, 6, 1); + } + return AsyncDeflate; +}()); +export { AsyncDeflate }; +export function deflate(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + return cbify(data, opts, [ + bDflt, + ], function (ev) { return pbf(deflateSync(ev.data[0], ev.data[1])); }, 0, cb); +} +/** + * Compresses data with DEFLATE without any wrapper + * @param data The data to compress + * @param opts The compression options + * @returns The deflated version of the data + */ +export function deflateSync(data, opts) { + return dopt(data, opts || {}, 0, 0); +} +/** + * Streaming DEFLATE decompression + */ +var Inflate = /*#__PURE__*/ (function () { + function Inflate(opts, cb) { + // no StrmOpt here to avoid adding to workerizer + if (typeof opts == 'function') + cb = opts, opts = {}; + this.ondata = cb; + var dict = opts && opts.dictionary && opts.dictionary.subarray(-32768); + this.s = { i: 0, b: dict ? dict.length : 0 }; + this.o = new u8(32768); + this.p = new u8(0); + if (dict) + this.o.set(dict); + } + Inflate.prototype.e = function (c) { + if (!this.ondata) + err(5); + if (this.d) + err(4); + if (!this.p.length) + this.p = c; + else if (c.length) { + var n = new u8(this.p.length + c.length); + n.set(this.p), n.set(c, this.p.length), this.p = n; + } + }; + Inflate.prototype.c = function (final) { + this.s.i = +(this.d = final || false); + var bts = this.s.b; + var dt = inflt(this.p, this.s, this.o); + this.ondata(slc(dt, bts, this.s.b), this.d); + this.o = slc(dt, this.s.b - 32768), this.s.b = this.o.length; + this.p = slc(this.p, (this.s.p / 8) | 0), this.s.p &= 7; + }; + /** + * Pushes a chunk to be inflated + * @param chunk The chunk to push + * @param final Whether this is the final chunk + */ + Inflate.prototype.push = function (chunk, final) { + this.e(chunk), this.c(final); + }; + return Inflate; +}()); +export { Inflate }; +/** + * Asynchronous streaming DEFLATE decompression + */ +var AsyncInflate = /*#__PURE__*/ (function () { + function AsyncInflate(opts, cb) { + astrmify([ + bInflt, + function () { return [astrm, Inflate]; } + ], this, StrmOpt.call(this, opts, cb), function (ev) { + var strm = new Inflate(ev.data); + onmessage = astrm(strm); + }, 7, 0); + } + return AsyncInflate; +}()); +export { AsyncInflate }; +export function inflate(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + return cbify(data, opts, [ + bInflt + ], function (ev) { return pbf(inflateSync(ev.data[0], gopt(ev.data[1]))); }, 1, cb); +} +/** + * Expands DEFLATE data with no wrapper + * @param data The data to decompress + * @param opts The decompression options + * @returns The decompressed version of the data + */ +export function inflateSync(data, opts) { + return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary); +} +// before you yell at me for not just using extends, my reason is that TS inheritance is hard to workerize. +/** + * Streaming GZIP compression + */ +var Gzip = /*#__PURE__*/ (function () { + function Gzip(opts, cb) { + this.c = crc(); + this.l = 0; + this.v = 1; + Deflate.call(this, opts, cb); + } + /** + * Pushes a chunk to be GZIPped + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + Gzip.prototype.push = function (chunk, final) { + this.c.p(chunk); + this.l += chunk.length; + Deflate.prototype.push.call(this, chunk, final); + }; + Gzip.prototype.p = function (c, f) { + var raw = dopt(c, this.o, this.v && gzhl(this.o), f && 8, this.s); + if (this.v) + gzh(raw, this.o), this.v = 0; + if (f) + wbytes(raw, raw.length - 8, this.c.d()), wbytes(raw, raw.length - 4, this.l); + this.ondata(raw, f); + }; + /** + * Flushes buffered uncompressed data. Useful to immediately retrieve the + * GZIPped output for small inputs. + */ + Gzip.prototype.flush = function () { + Deflate.prototype.flush.call(this); + }; + return Gzip; +}()); +export { Gzip }; +/** + * Asynchronous streaming GZIP compression + */ +var AsyncGzip = /*#__PURE__*/ (function () { + function AsyncGzip(opts, cb) { + astrmify([ + bDflt, + gze, + function () { return [astrm, Deflate, Gzip]; } + ], this, StrmOpt.call(this, opts, cb), function (ev) { + var strm = new Gzip(ev.data); + onmessage = astrm(strm); + }, 8, 1); + } + return AsyncGzip; +}()); +export { AsyncGzip }; +export function gzip(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + return cbify(data, opts, [ + bDflt, + gze, + function () { return [gzipSync]; } + ], function (ev) { return pbf(gzipSync(ev.data[0], ev.data[1])); }, 2, cb); +} +/** + * Compresses data with GZIP + * @param data The data to compress + * @param opts The compression options + * @returns The gzipped version of the data + */ +export function gzipSync(data, opts) { + if (!opts) + opts = {}; + var c = crc(), l = data.length; + c.p(data); + var d = dopt(data, opts, gzhl(opts), 8), s = d.length; + return gzh(d, opts), wbytes(d, s - 8, c.d()), wbytes(d, s - 4, l), d; +} +/** + * Streaming single or multi-member GZIP decompression + */ +var Gunzip = /*#__PURE__*/ (function () { + function Gunzip(opts, cb) { + this.v = 1; + this.r = 0; + Inflate.call(this, opts, cb); + } + /** + * Pushes a chunk to be GUNZIPped + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + Gunzip.prototype.push = function (chunk, final) { + Inflate.prototype.e.call(this, chunk); + this.r += chunk.length; + if (this.v) { + var p = this.p.subarray(this.v - 1); + var s = p.length > 3 ? gzs(p) : 4; + if (s > p.length) { + if (!final) + return; + } + else if (this.v > 1 && this.onmember) { + this.onmember(this.r - p.length); + } + this.p = p.subarray(s), this.v = 0; + } + // necessary to prevent TS from using the closure value + // This allows for workerization to function correctly + Inflate.prototype.c.call(this, final); + // process concatenated GZIP + if (this.s.f && !this.s.l && !final) { + this.v = shft(this.s.p) + 9; + this.s = { i: 0 }; + this.o = new u8(0); + this.push(new u8(0), final); + } + }; + return Gunzip; +}()); +export { Gunzip }; +/** + * Asynchronous streaming single or multi-member GZIP decompression + */ +var AsyncGunzip = /*#__PURE__*/ (function () { + function AsyncGunzip(opts, cb) { + var _this = this; + astrmify([ + bInflt, + guze, + function () { return [astrm, Inflate, Gunzip]; } + ], this, StrmOpt.call(this, opts, cb), function (ev) { + var strm = new Gunzip(ev.data); + strm.onmember = function (offset) { return postMessage(offset); }; + onmessage = astrm(strm); + }, 9, 0, function (offset) { return _this.onmember && _this.onmember(offset); }); + } + return AsyncGunzip; +}()); +export { AsyncGunzip }; +export function gunzip(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + return cbify(data, opts, [ + bInflt, + guze, + function () { return [gunzipSync]; } + ], function (ev) { return pbf(gunzipSync(ev.data[0], ev.data[1])); }, 3, cb); +} +/** + * Expands GZIP data + * @param data The data to decompress + * @param opts The decompression options + * @returns The decompressed version of the data + */ +export function gunzipSync(data, opts) { + var st = gzs(data); + if (st + 8 > data.length) + err(6, 'invalid gzip data'); + return inflt(data.subarray(st, -8), { i: 2 }, opts && opts.out || new u8(gzl(data)), opts && opts.dictionary); +} +/** + * Streaming Zlib compression + */ +var Zlib = /*#__PURE__*/ (function () { + function Zlib(opts, cb) { + this.c = adler(); + this.v = 1; + Deflate.call(this, opts, cb); + } + /** + * Pushes a chunk to be zlibbed + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + Zlib.prototype.push = function (chunk, final) { + this.c.p(chunk); + Deflate.prototype.push.call(this, chunk, final); + }; + Zlib.prototype.p = function (c, f) { + var raw = dopt(c, this.o, this.v && (this.o.dictionary ? 6 : 2), f && 4, this.s); + if (this.v) + zlh(raw, this.o), this.v = 0; + if (f) + wbytes(raw, raw.length - 4, this.c.d()); + this.ondata(raw, f); + }; + /** + * Flushes buffered uncompressed data. Useful to immediately retrieve the + * zlibbed output for small inputs. + */ + Zlib.prototype.flush = function () { + Deflate.prototype.flush.call(this); + }; + return Zlib; +}()); +export { Zlib }; +/** + * Asynchronous streaming Zlib compression + */ +var AsyncZlib = /*#__PURE__*/ (function () { + function AsyncZlib(opts, cb) { + astrmify([ + bDflt, + zle, + function () { return [astrm, Deflate, Zlib]; } + ], this, StrmOpt.call(this, opts, cb), function (ev) { + var strm = new Zlib(ev.data); + onmessage = astrm(strm); + }, 10, 1); + } + return AsyncZlib; +}()); +export { AsyncZlib }; +export function zlib(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + return cbify(data, opts, [ + bDflt, + zle, + function () { return [zlibSync]; } + ], function (ev) { return pbf(zlibSync(ev.data[0], ev.data[1])); }, 4, cb); +} +/** + * Compress data with Zlib + * @param data The data to compress + * @param opts The compression options + * @returns The zlib-compressed version of the data + */ +export function zlibSync(data, opts) { + if (!opts) + opts = {}; + var a = adler(); + a.p(data); + var d = dopt(data, opts, opts.dictionary ? 6 : 2, 4); + return zlh(d, opts), wbytes(d, d.length - 4, a.d()), d; +} +/** + * Streaming Zlib decompression + */ +var Unzlib = /*#__PURE__*/ (function () { + function Unzlib(opts, cb) { + Inflate.call(this, opts, cb); + this.v = opts && opts.dictionary ? 2 : 1; + } + /** + * Pushes a chunk to be unzlibbed + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + Unzlib.prototype.push = function (chunk, final) { + Inflate.prototype.e.call(this, chunk); + if (this.v) { + if (this.p.length < 6 && !final) + return; + this.p = this.p.subarray(zls(this.p, this.v - 1)), this.v = 0; + } + if (final) { + if (this.p.length < 4) + err(6, 'invalid zlib data'); + this.p = this.p.subarray(0, -4); + } + // necessary to prevent TS from using the closure value + // This allows for workerization to function correctly + Inflate.prototype.c.call(this, final); + }; + return Unzlib; +}()); +export { Unzlib }; +/** + * Asynchronous streaming Zlib decompression + */ +var AsyncUnzlib = /*#__PURE__*/ (function () { + function AsyncUnzlib(opts, cb) { + astrmify([ + bInflt, + zule, + function () { return [astrm, Inflate, Unzlib]; } + ], this, StrmOpt.call(this, opts, cb), function (ev) { + var strm = new Unzlib(ev.data); + onmessage = astrm(strm); + }, 11, 0); + } + return AsyncUnzlib; +}()); +export { AsyncUnzlib }; +export function unzlib(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + return cbify(data, opts, [ + bInflt, + zule, + function () { return [unzlibSync]; } + ], function (ev) { return pbf(unzlibSync(ev.data[0], gopt(ev.data[1]))); }, 5, cb); +} +/** + * Expands Zlib data + * @param data The data to decompress + * @param opts The decompression options + * @returns The decompressed version of the data + */ +export function unzlibSync(data, opts) { + return inflt(data.subarray(zls(data, opts && opts.dictionary), -4), { i: 2 }, opts && opts.out, opts && opts.dictionary); +} +// Default algorithm for compression (used because having a known output size allows faster decompression) +export { gzip as compress, AsyncGzip as AsyncCompress }; +export { gzipSync as compressSync, Gzip as Compress }; +/** + * Streaming GZIP, Zlib, or raw DEFLATE decompression + */ +var Decompress = /*#__PURE__*/ (function () { + function Decompress(opts, cb) { + this.o = StrmOpt.call(this, opts, cb) || {}; + this.G = Gunzip; + this.I = Inflate; + this.Z = Unzlib; + } + // init substream + // overriden by AsyncDecompress + Decompress.prototype.i = function () { + var _this = this; + this.s.ondata = function (dat, final) { + _this.ondata(dat, final); + }; + }; + /** + * Pushes a chunk to be decompressed + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + Decompress.prototype.push = function (chunk, final) { + if (!this.ondata) + err(5); + if (!this.s) { + if (this.p && this.p.length) { + var n = new u8(this.p.length + chunk.length); + n.set(this.p), n.set(chunk, this.p.length); + } + else + this.p = chunk; + if (this.p.length > 2) { + this.s = (this.p[0] == 31 && this.p[1] == 139 && this.p[2] == 8) + ? new this.G(this.o) + : ((this.p[0] & 15) != 8 || (this.p[0] >> 4) > 7 || ((this.p[0] << 8 | this.p[1]) % 31)) + ? new this.I(this.o) + : new this.Z(this.o); + this.i(); + this.s.push(this.p, final); + this.p = null; + } + } + else + this.s.push(chunk, final); + }; + return Decompress; +}()); +export { Decompress }; +/** + * Asynchronous streaming GZIP, Zlib, or raw DEFLATE decompression + */ +var AsyncDecompress = /*#__PURE__*/ (function () { + function AsyncDecompress(opts, cb) { + Decompress.call(this, opts, cb); + this.queuedSize = 0; + this.G = AsyncGunzip; + this.I = AsyncInflate; + this.Z = AsyncUnzlib; + } + AsyncDecompress.prototype.i = function () { + var _this = this; + this.s.ondata = function (err, dat, final) { + _this.ondata(err, dat, final); + }; + this.s.ondrain = function (size) { + _this.queuedSize -= size; + if (_this.ondrain) + _this.ondrain(size); + }; + }; + /** + * Pushes a chunk to be decompressed + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + AsyncDecompress.prototype.push = function (chunk, final) { + this.queuedSize += chunk.length; + Decompress.prototype.push.call(this, chunk, final); + }; + return AsyncDecompress; +}()); +export { AsyncDecompress }; +export function decompress(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + return (data[0] == 31 && data[1] == 139 && data[2] == 8) + ? gunzip(data, opts, cb) + : ((data[0] & 15) != 8 || (data[0] >> 4) > 7 || ((data[0] << 8 | data[1]) % 31)) + ? inflate(data, opts, cb) + : unzlib(data, opts, cb); +} +/** + * Expands compressed GZIP, Zlib, or raw DEFLATE data, automatically detecting the format + * @param data The data to decompress + * @param opts The decompression options + * @returns The decompressed version of the data + */ +export function decompressSync(data, opts) { + return (data[0] == 31 && data[1] == 139 && data[2] == 8) + ? gunzipSync(data, opts) + : ((data[0] & 15) != 8 || (data[0] >> 4) > 7 || ((data[0] << 8 | data[1]) % 31)) + ? inflateSync(data, opts) + : unzlibSync(data, opts); +} +// flatten a directory structure +var fltn = function (d, p, t, o) { + for (var k in d) { + var val = d[k], n = p + k, op = o; + if (Array.isArray(val)) + op = mrg(o, val[1]), val = val[0]; + if (val instanceof u8) + t[n] = [val, op]; + else { + t[n += '/'] = [new u8(0), op]; + fltn(val, n, t, o); + } + } +}; +// text encoder +var te = typeof TextEncoder != 'undefined' && /*#__PURE__*/ new TextEncoder(); +// text decoder +var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder(); +// text decoder stream +var tds = 0; +try { + td.decode(et, { stream: true }); + tds = 1; +} +catch (e) { } +// decode UTF8 +var dutf8 = function (d) { + for (var r = '', i = 0;;) { + var c = d[i++]; + var eb = (c > 127) + (c > 223) + (c > 239); + if (i + eb > d.length) + return { s: r, r: slc(d, i - 1) }; + if (!eb) + r += String.fromCharCode(c); + else if (eb == 3) { + c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | (d[i++] & 63)) - 65536, + r += String.fromCharCode(55296 | (c >> 10), 56320 | (c & 1023)); + } + else if (eb & 1) + r += String.fromCharCode((c & 31) << 6 | (d[i++] & 63)); + else + r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | (d[i++] & 63)); + } +}; +/** + * Streaming UTF-8 decoding + */ +var DecodeUTF8 = /*#__PURE__*/ (function () { + /** + * Creates a UTF-8 decoding stream + * @param cb The callback to call whenever data is decoded + */ + function DecodeUTF8(cb) { + this.ondata = cb; + if (tds) + this.t = new TextDecoder(); + else + this.p = et; + } + /** + * Pushes a chunk to be decoded from UTF-8 binary + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + DecodeUTF8.prototype.push = function (chunk, final) { + if (!this.ondata) + err(5); + final = !!final; + if (this.t) { + this.ondata(this.t.decode(chunk, { stream: true }), final); + if (final) { + if (this.t.decode().length) + err(8); + this.t = null; + } + return; + } + if (!this.p) + err(4); + var dat = new u8(this.p.length + chunk.length); + dat.set(this.p); + dat.set(chunk, this.p.length); + var _a = dutf8(dat), s = _a.s, r = _a.r; + if (final) { + if (r.length) + err(8); + this.p = null; + } + else + this.p = r; + this.ondata(s, final); + }; + return DecodeUTF8; +}()); +export { DecodeUTF8 }; +/** + * Streaming UTF-8 encoding + */ +var EncodeUTF8 = /*#__PURE__*/ (function () { + /** + * Creates a UTF-8 decoding stream + * @param cb The callback to call whenever data is encoded + */ + function EncodeUTF8(cb) { + this.ondata = cb; + } + /** + * Pushes a chunk to be encoded to UTF-8 + * @param chunk The string data to push + * @param final Whether this is the last chunk + */ + EncodeUTF8.prototype.push = function (chunk, final) { + if (!this.ondata) + err(5); + if (this.d) + err(4); + this.ondata(strToU8(chunk), this.d = final || false); + }; + return EncodeUTF8; +}()); +export { EncodeUTF8 }; +/** + * Converts a string into a Uint8Array for use with compression/decompression methods + * @param str The string to encode + * @param latin1 Whether or not to interpret the data as Latin-1. This should + * not need to be true unless decoding a binary string. + * @returns The string encoded in UTF-8/Latin-1 binary + */ +export function strToU8(str, latin1) { + if (latin1) { + var ar_1 = new u8(str.length); + for (var i = 0; i < str.length; ++i) + ar_1[i] = str.charCodeAt(i); + return ar_1; + } + if (te) + return te.encode(str); + var l = str.length; + var ar = new u8(str.length + (str.length >> 1)); + var ai = 0; + var w = function (v) { ar[ai++] = v; }; + for (var i = 0; i < l; ++i) { + if (ai + 5 > ar.length) { + var n = new u8(ai + 8 + ((l - i) << 1)); + n.set(ar); + ar = n; + } + var c = str.charCodeAt(i); + if (c < 128 || latin1) + w(c); + else if (c < 2048) + w(192 | (c >> 6)), w(128 | (c & 63)); + else if (c > 55295 && c < 57344) + c = 65536 + (c & 1023 << 10) | (str.charCodeAt(++i) & 1023), + w(240 | (c >> 18)), w(128 | ((c >> 12) & 63)), w(128 | ((c >> 6) & 63)), w(128 | (c & 63)); + else + w(224 | (c >> 12)), w(128 | ((c >> 6) & 63)), w(128 | (c & 63)); + } + return slc(ar, 0, ai); +} +/** + * Converts a Uint8Array to a string + * @param dat The data to decode to string + * @param latin1 Whether or not to interpret the data as Latin-1. This should + * not need to be true unless encoding to binary string. + * @returns The original UTF-8/Latin-1 string + */ +export function strFromU8(dat, latin1) { + if (latin1) { + var r = ''; + for (var i = 0; i < dat.length; i += 16384) + r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384)); + return r; + } + else if (td) { + return td.decode(dat); + } + else { + var _a = dutf8(dat), s = _a.s, r = _a.r; + if (r.length) + err(8); + return s; + } +} +; +// deflate bit flag +var dbf = function (l) { return l == 1 ? 3 : l < 6 ? 2 : l == 9 ? 1 : 0; }; +// skip local zip header +var slzh = function (d, b) { return b + 30 + b2(d, b + 26) + b2(d, b + 28); }; +// read zip header +var zh = function (d, b, z) { + var fnl = b2(d, b + 28), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl, bs = b4(d, b + 20); + var _a = z && bs == 4294967295 ? z64e(d, es) : [bs, b4(d, b + 24), b4(d, b + 42)], sc = _a[0], su = _a[1], off = _a[2]; + return [b2(d, b + 10), sc, su, fn, es + b2(d, b + 30) + b2(d, b + 32), off]; +}; +// read zip64 extra field +var z64e = function (d, b) { + for (; b2(d, b) != 1; b += 4 + b2(d, b + 2)) + ; + return [b8(d, b + 12), b8(d, b + 4), b8(d, b + 20)]; +}; +// extra field length +var exfl = function (ex) { + var le = 0; + if (ex) { + for (var k in ex) { + var l = ex[k].length; + if (l > 65535) + err(9); + le += l + 4; + } + } + return le; +}; +// write zip header +var wzh = function (d, b, f, fn, u, c, ce, co) { + var fl = fn.length, ex = f.extra, col = co && co.length; + var exl = exfl(ex); + wbytes(d, b, ce != null ? 0x2014B50 : 0x4034B50), b += 4; + if (ce != null) + d[b++] = 20, d[b++] = f.os; + d[b] = 20, b += 2; // spec compliance? what's that? + d[b++] = (f.flag << 1) | (c < 0 && 8), d[b++] = u && 8; + d[b++] = f.compression & 255, d[b++] = f.compression >> 8; + var dt = new Date(f.mtime == null ? Date.now() : f.mtime), y = dt.getFullYear() - 1980; + if (y < 0 || y > 119) + err(10); + wbytes(d, b, (y << 25) | ((dt.getMonth() + 1) << 21) | (dt.getDate() << 16) | (dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() >> 1)), b += 4; + if (c != -1) { + wbytes(d, b, f.crc); + wbytes(d, b + 4, c < 0 ? -c - 2 : c); + wbytes(d, b + 8, f.size); + } + wbytes(d, b + 12, fl); + wbytes(d, b + 14, exl), b += 16; + if (ce != null) { + wbytes(d, b, col); + wbytes(d, b + 6, f.attrs); + wbytes(d, b + 10, ce), b += 14; + } + d.set(fn, b); + b += fl; + if (exl) { + for (var k in ex) { + var exf = ex[k], l = exf.length; + wbytes(d, b, +k); + wbytes(d, b + 2, l); + d.set(exf, b + 4), b += 4 + l; + } + } + if (col) + d.set(co, b), b += col; + return b; +}; +// write zip footer (end of central directory) +var wzf = function (o, b, c, d, e) { + wbytes(o, b, 0x6054B50); // skip disk + wbytes(o, b + 8, c); + wbytes(o, b + 10, c); + wbytes(o, b + 12, d); + wbytes(o, b + 16, e); +}; +/** + * A pass-through stream to keep data uncompressed in a ZIP archive. + */ +var ZipPassThrough = /*#__PURE__*/ (function () { + /** + * Creates a pass-through stream that can be added to ZIP archives + * @param filename The filename to associate with this data stream + */ + function ZipPassThrough(filename) { + this.filename = filename; + this.c = crc(); + this.size = 0; + this.compression = 0; + } + /** + * Processes a chunk and pushes to the output stream. You can override this + * method in a subclass for custom behavior, but by default this passes + * the data through. You must call this.ondata(err, chunk, final) at some + * point in this method. + * @param chunk The chunk to process + * @param final Whether this is the last chunk + */ + ZipPassThrough.prototype.process = function (chunk, final) { + this.ondata(null, chunk, final); + }; + /** + * Pushes a chunk to be added. If you are subclassing this with a custom + * compression algorithm, note that you must push data from the source + * file only, pre-compression. + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + ZipPassThrough.prototype.push = function (chunk, final) { + if (!this.ondata) + err(5); + this.c.p(chunk); + this.size += chunk.length; + if (final) + this.crc = this.c.d(); + this.process(chunk, final || false); + }; + return ZipPassThrough; +}()); +export { ZipPassThrough }; +// I don't extend because TypeScript extension adds 1kB of runtime bloat +/** + * Streaming DEFLATE compression for ZIP archives. Prefer using AsyncZipDeflate + * for better performance + */ +var ZipDeflate = /*#__PURE__*/ (function () { + /** + * Creates a DEFLATE stream that can be added to ZIP archives + * @param filename The filename to associate with this data stream + * @param opts The compression options + */ + function ZipDeflate(filename, opts) { + var _this = this; + if (!opts) + opts = {}; + ZipPassThrough.call(this, filename); + this.d = new Deflate(opts, function (dat, final) { + _this.ondata(null, dat, final); + }); + this.compression = 8; + this.flag = dbf(opts.level); + } + ZipDeflate.prototype.process = function (chunk, final) { + try { + this.d.push(chunk, final); + } + catch (e) { + this.ondata(e, null, final); + } + }; + /** + * Pushes a chunk to be deflated + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + ZipDeflate.prototype.push = function (chunk, final) { + ZipPassThrough.prototype.push.call(this, chunk, final); + }; + return ZipDeflate; +}()); +export { ZipDeflate }; +/** + * Asynchronous streaming DEFLATE compression for ZIP archives + */ +var AsyncZipDeflate = /*#__PURE__*/ (function () { + /** + * Creates an asynchronous DEFLATE stream that can be added to ZIP archives + * @param filename The filename to associate with this data stream + * @param opts The compression options + */ + function AsyncZipDeflate(filename, opts) { + var _this = this; + if (!opts) + opts = {}; + ZipPassThrough.call(this, filename); + this.d = new AsyncDeflate(opts, function (err, dat, final) { + _this.ondata(err, dat, final); + }); + this.compression = 8; + this.flag = dbf(opts.level); + this.terminate = this.d.terminate; + } + AsyncZipDeflate.prototype.process = function (chunk, final) { + this.d.push(chunk, final); + }; + /** + * Pushes a chunk to be deflated + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + AsyncZipDeflate.prototype.push = function (chunk, final) { + ZipPassThrough.prototype.push.call(this, chunk, final); + }; + return AsyncZipDeflate; +}()); +export { AsyncZipDeflate }; +// TODO: Better tree shaking +/** + * A zippable archive to which files can incrementally be added + */ +var Zip = /*#__PURE__*/ (function () { + /** + * Creates an empty ZIP archive to which files can be added + * @param cb The callback to call whenever data for the generated ZIP archive + * is available + */ + function Zip(cb) { + this.ondata = cb; + this.u = []; + this.d = 1; + } + /** + * Adds a file to the ZIP archive + * @param file The file stream to add + */ + Zip.prototype.add = function (file) { + var _this = this; + if (!this.ondata) + err(5); + // finishing or finished + if (this.d & 2) + this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, false); + else { + var f = strToU8(file.filename), fl_1 = f.length; + var com = file.comment, o = com && strToU8(com); + var u = fl_1 != file.filename.length || (o && (com.length != o.length)); + var hl_1 = fl_1 + exfl(file.extra) + 30; + if (fl_1 > 65535) + this.ondata(err(11, 0, 1), null, false); + var header = new u8(hl_1); + wzh(header, 0, file, f, u, -1); + var chks_1 = [header]; + var pAll_1 = function () { + for (var _i = 0, chks_2 = chks_1; _i < chks_2.length; _i++) { + var chk = chks_2[_i]; + _this.ondata(null, chk, false); + } + chks_1 = []; + }; + var tr_1 = this.d; + this.d = 0; + var ind_1 = this.u.length; + var uf_1 = mrg(file, { + f: f, + u: u, + o: o, + t: function () { + if (file.terminate) + file.terminate(); + }, + r: function () { + pAll_1(); + if (tr_1) { + var nxt = _this.u[ind_1 + 1]; + if (nxt) + nxt.r(); + else + _this.d = 1; + } + tr_1 = 1; + } + }); + var cl_1 = 0; + file.ondata = function (err, dat, final) { + if (err) { + _this.ondata(err, dat, final); + _this.terminate(); + } + else { + cl_1 += dat.length; + chks_1.push(dat); + if (final) { + var dd = new u8(16); + wbytes(dd, 0, 0x8074B50); + wbytes(dd, 4, file.crc); + wbytes(dd, 8, cl_1); + wbytes(dd, 12, file.size); + chks_1.push(dd); + uf_1.c = cl_1, uf_1.b = hl_1 + cl_1 + 16, uf_1.crc = file.crc, uf_1.size = file.size; + if (tr_1) + uf_1.r(); + tr_1 = 1; + } + else if (tr_1) + pAll_1(); + } + }; + this.u.push(uf_1); + } + }; + /** + * Ends the process of adding files and prepares to emit the final chunks. + * This *must* be called after adding all desired files for the resulting + * ZIP file to work properly. + */ + Zip.prototype.end = function () { + var _this = this; + if (this.d & 2) { + this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, true); + return; + } + if (this.d) + this.e(); + else + this.u.push({ + r: function () { + if (!(_this.d & 1)) + return; + _this.u.splice(-1, 1); + _this.e(); + }, + t: function () { } + }); + this.d = 3; + }; + Zip.prototype.e = function () { + var bt = 0, l = 0, tl = 0; + for (var _i = 0, _a = this.u; _i < _a.length; _i++) { + var f = _a[_i]; + tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0); + } + var out = new u8(tl + 22); + for (var _b = 0, _c = this.u; _b < _c.length; _b++) { + var f = _c[_b]; + wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o); + bt += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b; + } + wzf(out, bt, this.u.length, tl, l); + this.ondata(null, out, true); + this.d = 2; + }; + /** + * A method to terminate any internal workers used by the stream. Subsequent + * calls to add() will fail. + */ + Zip.prototype.terminate = function () { + for (var _i = 0, _a = this.u; _i < _a.length; _i++) { + var f = _a[_i]; + f.t(); + } + this.d = 2; + }; + return Zip; +}()); +export { Zip }; +export function zip(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + var r = {}; + fltn(data, '', r, opts); + var k = Object.keys(r); + var lft = k.length, o = 0, tot = 0; + var slft = lft, files = new Array(lft); + var term = []; + var tAll = function () { + for (var i = 0; i < term.length; ++i) + term[i](); + }; + var cbd = function (a, b) { + mt(function () { cb(a, b); }); + }; + mt(function () { cbd = cb; }); + var cbf = function () { + var out = new u8(tot + 22), oe = o, cdl = tot - o; + tot = 0; + for (var i = 0; i < slft; ++i) { + var f = files[i]; + try { + var l = f.c.length; + wzh(out, tot, f, f.f, f.u, l); + var badd = 30 + f.f.length + exfl(f.extra); + var loc = tot + badd; + out.set(f.c, loc); + wzh(out, o, f, f.f, f.u, l, tot, f.m), o += 16 + badd + (f.m ? f.m.length : 0), tot = loc + l; + } + catch (e) { + return cbd(e, null); + } + } + wzf(out, o, files.length, cdl, oe); + cbd(null, out); + }; + if (!lft) + cbf(); + var _loop_1 = function (i) { + var fn = k[i]; + var _a = r[fn], file = _a[0], p = _a[1]; + var c = crc(), size = file.length; + c.p(file); + var f = strToU8(fn), s = f.length; + var com = p.comment, m = com && strToU8(com), ms = m && m.length; + var exl = exfl(p.extra); + var compression = p.level == 0 ? 0 : 8; + var cbl = function (e, d) { + if (e) { + tAll(); + cbd(e, null); + } + else { + var l = d.length; + files[i] = mrg(p, { + size: size, + crc: c.d(), + c: d, + f: f, + m: m, + u: s != fn.length || (m && (com.length != ms)), + compression: compression + }); + o += 30 + s + exl + l; + tot += 76 + 2 * (s + exl) + (ms || 0) + l; + if (!--lft) + cbf(); + } + }; + if (s > 65535) + cbl(err(11, 0, 1), null); + if (!compression) + cbl(null, file); + else if (size < 160000) { + try { + cbl(null, deflateSync(file, p)); + } + catch (e) { + cbl(e, null); + } + } + else + term.push(deflate(file, p, cbl)); + }; + // Cannot use lft because it can decrease + for (var i = 0; i < slft; ++i) { + _loop_1(i); + } + return tAll; +} +/** + * Synchronously creates a ZIP file. Prefer using `zip` for better performance + * with more than one file. + * @param data The directory structure for the ZIP archive + * @param opts The main options, merged with per-file options + * @returns The generated ZIP archive + */ +export function zipSync(data, opts) { + if (!opts) + opts = {}; + var r = {}; + var files = []; + fltn(data, '', r, opts); + var o = 0; + var tot = 0; + for (var fn in r) { + var _a = r[fn], file = _a[0], p = _a[1]; + var compression = p.level == 0 ? 0 : 8; + var f = strToU8(fn), s = f.length; + var com = p.comment, m = com && strToU8(com), ms = m && m.length; + var exl = exfl(p.extra); + if (s > 65535) + err(11); + var d = compression ? deflateSync(file, p) : file, l = d.length; + var c = crc(); + c.p(file); + files.push(mrg(p, { + size: file.length, + crc: c.d(), + c: d, + f: f, + m: m, + u: s != fn.length || (m && (com.length != ms)), + o: o, + compression: compression + })); + o += 30 + s + exl + l; + tot += 76 + 2 * (s + exl) + (ms || 0) + l; + } + var out = new u8(tot + 22), oe = o, cdl = tot - o; + for (var i = 0; i < files.length; ++i) { + var f = files[i]; + wzh(out, f.o, f, f.f, f.u, f.c.length); + var badd = 30 + f.f.length + exfl(f.extra); + out.set(f.c, f.o + badd); + wzh(out, o, f, f.f, f.u, f.c.length, f.o, f.m), o += 16 + badd + (f.m ? f.m.length : 0); + } + wzf(out, o, files.length, cdl, oe); + return out; +} +/** + * Streaming pass-through decompression for ZIP archives + */ +var UnzipPassThrough = /*#__PURE__*/ (function () { + function UnzipPassThrough() { + } + UnzipPassThrough.prototype.push = function (data, final) { + this.ondata(null, data, final); + }; + UnzipPassThrough.compression = 0; + return UnzipPassThrough; +}()); +export { UnzipPassThrough }; +/** + * Streaming DEFLATE decompression for ZIP archives. Prefer AsyncZipInflate for + * better performance. + */ +var UnzipInflate = /*#__PURE__*/ (function () { + /** + * Creates a DEFLATE decompression that can be used in ZIP archives + */ + function UnzipInflate() { + var _this = this; + this.i = new Inflate(function (dat, final) { + _this.ondata(null, dat, final); + }); + } + UnzipInflate.prototype.push = function (data, final) { + try { + this.i.push(data, final); + } + catch (e) { + this.ondata(e, null, final); + } + }; + UnzipInflate.compression = 8; + return UnzipInflate; +}()); +export { UnzipInflate }; +/** + * Asynchronous streaming DEFLATE decompression for ZIP archives + */ +var AsyncUnzipInflate = /*#__PURE__*/ (function () { + /** + * Creates a DEFLATE decompression that can be used in ZIP archives + */ + function AsyncUnzipInflate(_, sz) { + var _this = this; + if (sz < 320000) { + this.i = new Inflate(function (dat, final) { + _this.ondata(null, dat, final); + }); + } + else { + this.i = new AsyncInflate(function (err, dat, final) { + _this.ondata(err, dat, final); + }); + this.terminate = this.i.terminate; + } + } + AsyncUnzipInflate.prototype.push = function (data, final) { + if (this.i.terminate) + data = slc(data, 0); + this.i.push(data, final); + }; + AsyncUnzipInflate.compression = 8; + return AsyncUnzipInflate; +}()); +export { AsyncUnzipInflate }; +/** + * A ZIP archive decompression stream that emits files as they are discovered + */ +var Unzip = /*#__PURE__*/ (function () { + /** + * Creates a ZIP decompression stream + * @param cb The callback to call whenever a file in the ZIP archive is found + */ + function Unzip(cb) { + this.onfile = cb; + this.k = []; + this.o = { + 0: UnzipPassThrough + }; + this.p = et; + } + /** + * Pushes a chunk to be unzipped + * @param chunk The chunk to push + * @param final Whether this is the last chunk + */ + Unzip.prototype.push = function (chunk, final) { + var _this = this; + if (!this.onfile) + err(5); + if (!this.p) + err(4); + if (this.c > 0) { + var len = Math.min(this.c, chunk.length); + var toAdd = chunk.subarray(0, len); + this.c -= len; + if (this.d) + this.d.push(toAdd, !this.c); + else + this.k[0].push(toAdd); + chunk = chunk.subarray(len); + if (chunk.length) + return this.push(chunk, final); + } + else { + var f = 0, i = 0, is = void 0, buf = void 0; + if (!this.p.length) + buf = chunk; + else if (!chunk.length) + buf = this.p; + else { + buf = new u8(this.p.length + chunk.length); + buf.set(this.p), buf.set(chunk, this.p.length); + } + var l = buf.length, oc = this.c, add = oc && this.d; + var _loop_2 = function () { + var _a; + var sig = b4(buf, i); + if (sig == 0x4034B50) { + f = 1, is = i; + this_1.d = null; + this_1.c = 0; + var bf = b2(buf, i + 6), cmp_1 = b2(buf, i + 8), u = bf & 2048, dd = bf & 8, fnl = b2(buf, i + 26), es = b2(buf, i + 28); + if (l > i + 30 + fnl + es) { + var chks_3 = []; + this_1.k.unshift(chks_3); + f = 2; + var sc_1 = b4(buf, i + 18), su_1 = b4(buf, i + 22); + var fn_1 = strFromU8(buf.subarray(i + 30, i += 30 + fnl), !u); + if (sc_1 == 4294967295) { + _a = dd ? [-2] : z64e(buf, i), sc_1 = _a[0], su_1 = _a[1]; + } + else if (dd) + sc_1 = -1; + i += es; + this_1.c = sc_1; + var d_1; + var file_1 = { + name: fn_1, + compression: cmp_1, + start: function () { + if (!file_1.ondata) + err(5); + if (!sc_1) + file_1.ondata(null, et, true); + else { + var ctr = _this.o[cmp_1]; + if (!ctr) + file_1.ondata(err(14, 'unknown compression type ' + cmp_1, 1), null, false); + d_1 = sc_1 < 0 ? new ctr(fn_1) : new ctr(fn_1, sc_1, su_1); + d_1.ondata = function (err, dat, final) { file_1.ondata(err, dat, final); }; + for (var _i = 0, chks_4 = chks_3; _i < chks_4.length; _i++) { + var dat = chks_4[_i]; + d_1.push(dat, false); + } + if (_this.k[0] == chks_3 && _this.c) + _this.d = d_1; + else + d_1.push(et, true); + } + }, + terminate: function () { + if (d_1 && d_1.terminate) + d_1.terminate(); + } + }; + if (sc_1 >= 0) + file_1.size = sc_1, file_1.originalSize = su_1; + this_1.onfile(file_1); + } + return "break"; + } + else if (oc) { + if (sig == 0x8074B50) { + is = i += 12 + (oc == -2 && 8), f = 3, this_1.c = 0; + return "break"; + } + else if (sig == 0x2014B50) { + is = i -= 4, f = 3, this_1.c = 0; + return "break"; + } + } + }; + var this_1 = this; + for (; i < l - 4; ++i) { + var state_1 = _loop_2(); + if (state_1 === "break") + break; + } + this.p = et; + if (oc < 0) { + var dat = f ? buf.subarray(0, is - 12 - (oc == -2 && 8) - (b4(buf, is - 16) == 0x8074B50 && 4)) : buf.subarray(0, i); + if (add) + add.push(dat, !!f); + else + this.k[+(f == 2)].push(dat); + } + if (f & 2) + return this.push(buf.subarray(i), final); + this.p = buf.subarray(i); + } + if (final) { + if (this.c) + err(13); + this.p = null; + } + }; + /** + * Registers a decoder with the stream, allowing for files compressed with + * the compression type provided to be expanded correctly + * @param decoder The decoder constructor + */ + Unzip.prototype.register = function (decoder) { + this.o[decoder.compression] = decoder; + }; + return Unzip; +}()); +export { Unzip }; +var mt = typeof queueMicrotask == 'function' ? queueMicrotask : typeof setTimeout == 'function' ? setTimeout : function (fn) { fn(); }; +export function unzip(data, opts, cb) { + if (!cb) + cb = opts, opts = {}; + if (typeof cb != 'function') + err(7); + var term = []; + var tAll = function () { + for (var i = 0; i < term.length; ++i) + term[i](); + }; + var files = {}; + var cbd = function (a, b) { + mt(function () { cb(a, b); }); + }; + mt(function () { cbd = cb; }); + var e = data.length - 22; + for (; b4(data, e) != 0x6054B50; --e) { + if (!e || data.length - e > 65558) { + cbd(err(13, 0, 1), null); + return tAll; + } + } + ; + var lft = b2(data, e + 8); + if (lft) { + var c = lft; + var o = b4(data, e + 16); + var z = o == 4294967295 || c == 65535; + if (z) { + var ze = b4(data, e - 12); + z = b4(data, ze) == 0x6064B50; + if (z) { + c = lft = b4(data, ze + 32); + o = b4(data, ze + 48); + } + } + var fltr = opts && opts.filter; + var _loop_3 = function (i) { + var _a = zh(data, o, z), c_1 = _a[0], sc = _a[1], su = _a[2], fn = _a[3], no = _a[4], off = _a[5], b = slzh(data, off); + o = no; + var cbl = function (e, d) { + if (e) { + tAll(); + cbd(e, null); + } + else { + if (d) + files[fn] = d; + if (!--lft) + cbd(null, files); + } + }; + if (!fltr || fltr({ + name: fn, + size: sc, + originalSize: su, + compression: c_1 + })) { + if (!c_1) + cbl(null, slc(data, b, b + sc)); + else if (c_1 == 8) { + var infl = data.subarray(b, b + sc); + // Synchronously decompress under 512KB, or barely-compressed data + if (su < 524288 || sc > 0.8 * su) { + try { + cbl(null, inflateSync(infl, { out: new u8(su) })); + } + catch (e) { + cbl(e, null); + } + } + else + term.push(inflate(infl, { size: su }, cbl)); + } + else + cbl(err(14, 'unknown compression type ' + c_1, 1), null); + } + else + cbl(null, null); + }; + for (var i = 0; i < c; ++i) { + _loop_3(i); + } + } + else + cbd(null, {}); + return tAll; +} +/** + * Synchronously decompresses a ZIP archive. Prefer using `unzip` for better + * performance with more than one file. + * @param data The raw compressed ZIP file + * @param opts The ZIP extraction options + * @returns The decompressed files + */ +export function unzipSync(data, opts) { + var files = {}; + var e = data.length - 22; + for (; b4(data, e) != 0x6054B50; --e) { + if (!e || data.length - e > 65558) + err(13); + } + ; + var c = b2(data, e + 8); + if (!c) + return {}; + var o = b4(data, e + 16); + var z = o == 4294967295 || c == 65535; + if (z) { + var ze = b4(data, e - 12); + z = b4(data, ze) == 0x6064B50; + if (z) { + c = b4(data, ze + 32); + o = b4(data, ze + 48); + } + } + var fltr = opts && opts.filter; + for (var i = 0; i < c; ++i) { + var _a = zh(data, o, z), c_2 = _a[0], sc = _a[1], su = _a[2], fn = _a[3], no = _a[4], off = _a[5], b = slzh(data, off); + o = no; + if (!fltr || fltr({ + name: fn, + size: sc, + originalSize: su, + compression: c_2 + })) { + if (!c_2) + files[fn] = slc(data, b, b + sc); + else if (c_2 == 8) + files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) }); + else + err(14, 'unknown compression type ' + c_2); + } + } + return files; +} diff --git a/libs/jieba-wasm/LICENSE b/libs/jieba-wasm/LICENSE new file mode 100644 index 0000000..5a792c4 --- /dev/null +++ b/libs/jieba-wasm/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2018 fengkx + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/libs/jieba-wasm/README.md b/libs/jieba-wasm/README.md new file mode 100644 index 0000000..a438327 --- /dev/null +++ b/libs/jieba-wasm/README.md @@ -0,0 +1,134 @@ +# jieba-wasm + +> [jieba-rs](https://github.com/messense/jieba-rs) 的 wasm binding + +_编译成 WASM 摆脱编译 Node Addon 的烦恼_ + +# Usage +## Node.js +```js +const { + cut, + cut_all, + cut_for_search, + tokenize, + add_word, +} = require("jieba-wasm"); +cut("中华人民共和国武汉市长江大桥", true); +// [ '中华人民共和国', '武汉市', '长江大桥' ] +cut_all("中华人民共和国武汉市长江大桥", true); +/* +[ + '中', '中华', + '中华人民', '中华人民共和国', + '华', '华人', + '人', '人民', + '人民共和国', '民', + '共', '共和', + '共和国', '和', + '国', '武', + '武汉', '武汉市', + '汉', '市', + '市长', '长', + '长江', '长江大桥', + '江', '大', + '大桥', '桥' +] +*/ +cut_for_search("中华人民共和国武汉市长江大桥", true); +/* +[ + '中华', '华人', + '人民', '共和', + '共和国', '中华人民共和国', + '武汉', '武汉市', + '长江', '大桥', + '长江大桥' +] +*/ +tokenize("中华人民共和国武汉市长江大桥", "default", true); +/* +[ + { word: '中华人民共和国', start: 0, end: 7 }, + { word: '武汉市', start: 7, end: 10 }, + { word: '长江大桥', start: 10, end: 14 } +] +*/ +tokenize("中华人民共和国武汉市长江大桥", "search", true); +/* +[ + { word: '中华', start: 0, end: 2 }, + { word: '华人', start: 1, end: 3 }, + { word: '人民', start: 2, end: 4 }, + { word: '共和', start: 4, end: 6 }, + { word: '共和国', start: 4, end: 7 }, + { word: '中华人民共和国', start: 0, end: 7 }, + { word: '武汉', start: 7, end: 9 }, + { word: '武汉市', start: 7, end: 10 }, + { word: '长江', start: 10, end: 12 }, + { word: '大桥', start: 12, end: 14 }, + { word: '长江大桥', start: 10, end: 14 } +] +*/ + +cut("桥大江长市汉武的省北湖国和共民人华中"); +/* +[ + '桥', '大江', '长', + '市', '汉', '武', + '的', '省', '北湖', + '国', '和', '共', + '民', '人', '华中' +] +*/ +["桥大江长", "市汉武", "省北湖", "国和共民人华中"].map((word) => { + add_word(word); +}); +cut("桥大江长市汉武的省北湖国和共民人华中"); +// ["桥大江长", "市汉武", "的", "省北湖", "国和共民人华中"]; + +with_dict("自动借书机 1 n"); // 导入自定义字典,词条格式:词语 词频 词性(可选),以换行符分隔 +cut("你好我是一个自动借书机"); +// ["你好", "我", "是", "一个", "自动借书机"]; +``` + +## Browser +```ts +import init, { cut } from 'jieba-wasm'; + +// 重要:使用前必须初始化 +await init(); + +cut("中华人民共和国武汉市长江大桥", true); +// [ '中华人民共和国', '武汉市', '长江大桥' ] +``` + +# 示例 Demo + +## 安装依赖 + +安装 wasm-bindgen 和 wasm-opt + +```bash +cargo install wasm-bindgen-cli --locked +cargo install wasm-opt --locked +``` + +## 前期准备 + +首先保证存在 rust 环境,然后运行以下命令 +```bash +npm run build:cargo +npm run build +``` + +## 运行浏览器端示例 +```bash +cd demo/web +npm install +npm run dev +``` + +# Piror Art + +https://github.com/messense/jieba-rs diff --git a/libs/jieba-wasm/jieba_rs_wasm.d.ts b/libs/jieba-wasm/jieba_rs_wasm.d.ts new file mode 100644 index 0000000..fa7c50a --- /dev/null +++ b/libs/jieba-wasm/jieba_rs_wasm.d.ts @@ -0,0 +1,73 @@ +/* tslint:disable */ +/* eslint-disable */ +export function cut(text: string, hmm?: boolean | null): string[]; +export function cut_all(text: string): string[]; +export function cut_for_search(text: string, hmm?: boolean | null): string[]; +export function tokenize(text: string, mode: string, hmm?: boolean | null): Token[]; +export function add_word(word: string, freq?: number | null, tag?: string | null): number; +export function tag(sentence: string, hmm?: boolean | null): Tag[]; +export function with_dict(dict: string): void; + +/** Represents a single token with its word and position. */ +export interface Token { + word: string; + start: number; + end: number; +} + +/** Represents a single word and its part-of-speech tag. */ +export interface Tag { + word: string; + tag: string; +} + + + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly cut: (a: number, b: number, c: number) => [number, number]; + readonly cut_all: (a: number, b: number) => [number, number]; + readonly cut_for_search: (a: number, b: number, c: number) => [number, number]; + readonly tokenize: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; + readonly add_word: (a: number, b: number, c: number, d: number, e: number) => number; + readonly tag: (a: number, b: number, c: number) => [number, number]; + readonly with_dict: (a: number, b: number) => [number, number]; + readonly rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void; + readonly rust_zstd_wasm_shim_malloc: (a: number) => number; + readonly rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number; + readonly rust_zstd_wasm_shim_calloc: (a: number, b: number) => number; + readonly rust_zstd_wasm_shim_free: (a: number) => void; + readonly rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number; + readonly rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number; + readonly rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_export_2: WebAssembly.Table; + readonly __externref_drop_slice: (a: number, b: number) => void; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __externref_table_dealloc: (a: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. +* +* @returns {InitOutput} +*/ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. +* +* @returns {Promise} +*/ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/libs/jieba-wasm/jieba_rs_wasm.js b/libs/jieba-wasm/jieba_rs_wasm.js new file mode 100644 index 0000000..7281ce6 --- /dev/null +++ b/libs/jieba-wasm/jieba_rs_wasm.js @@ -0,0 +1,438 @@ +let wasm; + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +let cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; + +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedDataViewMemory0 = null; + +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function getArrayJsValueFromWasm0(ptr, len) { + ptr = ptr >>> 0; + const mem = getDataViewMemory0(); + const result = []; + for (let i = ptr; i < ptr + 4 * len; i += 4) { + result.push(wasm.__wbindgen_export_2.get(mem.getUint32(i, true))); + } + wasm.__externref_drop_slice(ptr, len); + return result; +} +/** + * @param {string} text + * @param {boolean | null} [hmm] + * @returns {string[]} + */ +export function cut(text, hmm) { + const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.cut(ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0); + var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v2; +} + +/** + * @param {string} text + * @returns {string[]} + */ +export function cut_all(text) { + const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.cut_all(ptr0, len0); + var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v2; +} + +/** + * @param {string} text + * @param {boolean | null} [hmm] + * @returns {string[]} + */ +export function cut_for_search(text, hmm) { + const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.cut_for_search(ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0); + var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v2; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_export_2.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} +/** + * @param {string} text + * @param {string} mode + * @param {boolean | null} [hmm] + * @returns {Token[]} + */ +export function tokenize(text, mode, hmm) { + const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(mode, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.tokenize(ptr0, len0, ptr1, len1, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0); + if (ret[3]) { + throw takeFromExternrefTable0(ret[2]); + } + var v3 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v3; +} + +/** + * @param {string} word + * @param {number | null} [freq] + * @param {string | null} [tag] + * @returns {number} + */ +export function add_word(word, freq, tag) { + const ptr0 = passStringToWasm0(word, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + var ptr1 = isLikeNone(tag) ? 0 : passStringToWasm0(tag, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + const ret = wasm.add_word(ptr0, len0, isLikeNone(freq) ? 0x100000001 : (freq) >>> 0, ptr1, len1); + return ret >>> 0; +} + +/** + * @param {string} sentence + * @param {boolean | null} [hmm] + * @returns {Tag[]} + */ +export function tag(sentence, hmm) { + const ptr0 = passStringToWasm0(sentence, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.tag(ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0); + var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v2; +} + +/** + * @param {string} dict + */ +export function with_dict(dict) { + const ptr0 = passStringToWasm0(dict, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.with_dict(ptr0, len0); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } +} + +const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_Error_0497d5bdba9362e5 = function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbg_new_07b483f72211fd66 = function() { + const ret = new Object(); + return ret; + }; + imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) { + arg0[arg1] = arg2; + }; + imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) { + const ret = BigInt.asUintN(64, arg0); + return ret; + }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_export_2; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + ; + }; + imports.wbg.__wbindgen_number_new = function(arg0) { + const ret = arg0; + return ret; + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + + return imports; +} + +function __wbg_init_memory(imports, memory) { + +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('jieba_rs_wasm_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/libs/jieba-wasm/jieba_rs_wasm_bg.wasm b/libs/jieba-wasm/jieba_rs_wasm_bg.wasm new file mode 100644 index 0000000..92df1dc Binary files /dev/null and b/libs/jieba-wasm/jieba_rs_wasm_bg.wasm differ diff --git a/libs/jieba-wasm/jieba_rs_wasm_bg.wasm.d.ts b/libs/jieba-wasm/jieba_rs_wasm_bg.wasm.d.ts new file mode 100644 index 0000000..ab7e1cd --- /dev/null +++ b/libs/jieba-wasm/jieba_rs_wasm_bg.wasm.d.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const cut: (a: number, b: number, c: number) => [number, number]; +export const cut_all: (a: number, b: number) => [number, number]; +export const cut_for_search: (a: number, b: number, c: number) => [number, number]; +export const tokenize: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number]; +export const add_word: (a: number, b: number, c: number, d: number, e: number) => number; +export const tag: (a: number, b: number, c: number) => [number, number]; +export const with_dict: (a: number, b: number) => [number, number]; +export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void; +export const rust_zstd_wasm_shim_malloc: (a: number) => number; +export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number; +export const rust_zstd_wasm_shim_calloc: (a: number, b: number) => number; +export const rust_zstd_wasm_shim_free: (a: number) => void; +export const rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number; +export const rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number; +export const rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_export_2: WebAssembly.Table; +export const __externref_drop_slice: (a: number, b: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_start: () => void; diff --git a/libs/jieba-wasm/package.json b/libs/jieba-wasm/package.json new file mode 100644 index 0000000..cea989b --- /dev/null +++ b/libs/jieba-wasm/package.json @@ -0,0 +1,129 @@ +{ + "name": "jieba-wasm", + "version": "2.4.0", + "description": "WASM binding to jieba-rs", + "main": "./pkg/nodejs/jieba_rs_wasm.js", + "types": "./pkg/nodejs/jieba_rs_wasm.d.ts", + "exports": { + ".": { + "node": { + "types": "./pkg/nodejs/jieba_rs_wasm.d.ts", + "default": "./pkg/nodejs/jieba_rs_wasm.js" + }, + "deno": { + "types": "./pkg/deno/jieba_rs_wasm.d.ts", + "default": "./pkg/deno/jieba_rs_wasm.js" + }, + "browser": { + "types": "./pkg/web/jieba_rs_wasm.d.ts", + "default": "./pkg/web/jieba_rs_wasm.js" + }, + "import": { + "types": "./pkg/web/jieba_rs_wasm.d.ts", + "default": "./pkg/web/jieba_rs_wasm.js" + }, + "require": { + "types": "./pkg/nodejs/jieba_rs_wasm.d.ts", + "default": "./pkg/nodejs/jieba_rs_wasm.js" + } + }, + "./web": { + "types": "./pkg/web/jieba_rs_wasm.d.ts", + "default": "./pkg/web/jieba_rs_wasm.js" + }, + "./node": { + "types": "./pkg/nodejs/jieba_rs_wasm.d.ts", + "default": "./pkg/nodejs/jieba_rs_wasm.js" + }, + "./deno": { + "types": "./pkg/deno/jieba_rs_wasm.d.ts", + "default": "./pkg/deno/jieba_rs_wasm.js" + } + }, + "directories": { + "test": "tests" + }, + "scripts": { + "build": "wireit", + "build:cargo": "wireit", + "build:bundler": "wireit", + "build:nodejs": "wireit", + "build:deno": "wireit", + "build:web": "wireit", + "build:opt": "wireit", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "wireit": { + "build:cargo": { + "command": "cargo build --release --target wasm32-unknown-unknown" + }, + "build:bundler": { + "command": "wasm-bindgen target/wasm32-unknown-unknown/release/jieba_rs_wasm.wasm --out-dir ./pkg/bundler --target bundler", + "dependencies": [ + "build:cargo" + ] + }, + "build:nodejs": { + "command": "wasm-bindgen target/wasm32-unknown-unknown/release/jieba_rs_wasm.wasm --out-dir ./pkg/nodejs --target nodejs", + "dependencies": [ + "build:cargo" + ] + }, + "build:deno": { + "command": "wasm-bindgen target/wasm32-unknown-unknown/release/jieba_rs_wasm.wasm --out-dir ./pkg/deno --target deno", + "dependencies": [ + "build:cargo" + ] + }, + "build:web": { + "command": "wasm-bindgen target/wasm32-unknown-unknown/release/jieba_rs_wasm.wasm --out-dir ./pkg/web --target web", + "dependencies": [ + "build:cargo" + ] + }, + "build": { + "dependencies": [ + "build:cargo", + "build:bundler", + "build:nodejs", + "build:deno", + "build:web", + "build:opt" + ] + }, + "build:opt": { + "command": "node scripts/opt.js", + "dependencies": [ + "build:cargo", + "build:bundler", + "build:nodejs", + "build:deno", + "build:web" + ] + } + }, + "files": [ + "pkg/**/*" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/fengkx/jieba-wasm.git" + }, + "keywords": [ + "wasm", + "jieba", + "chinese", + "segment", + "中文分词" + ], + "author": "fengkx", + "license": "MIT", + "bugs": { + "url": "https://github.com/fengkx/jieba-wasm/issues" + }, + "homepage": "https://github.com/fengkx/jieba-wasm#readme", + "devDependencies": { + "@jsdevtools/ez-spawn": "^3.0.4", + "wireit": "^0.14.4" + } +} diff --git a/libs/js-yaml.mjs b/libs/js-yaml.mjs new file mode 100644 index 0000000..be71cad --- /dev/null +++ b/libs/js-yaml.mjs @@ -0,0 +1,3851 @@ + +/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */ +function isNothing(subject) { + return (typeof subject === 'undefined') || (subject === null); +} + + +function isObject(subject) { + return (typeof subject === 'object') && (subject !== null); +} + + +function toArray(sequence) { + if (Array.isArray(sequence)) return sequence; + else if (isNothing(sequence)) return []; + + return [ sequence ]; +} + + +function extend(target, source) { + var index, length, key, sourceKeys; + + if (source) { + sourceKeys = Object.keys(source); + + for (index = 0, length = sourceKeys.length; index < length; index += 1) { + key = sourceKeys[index]; + target[key] = source[key]; + } + } + + return target; +} + + +function repeat(string, count) { + var result = '', cycle; + + for (cycle = 0; cycle < count; cycle += 1) { + result += string; + } + + return result; +} + + +function isNegativeZero(number) { + return (number === 0) && (Number.NEGATIVE_INFINITY === 1 / number); +} + + +var isNothing_1 = isNothing; +var isObject_1 = isObject; +var toArray_1 = toArray; +var repeat_1 = repeat; +var isNegativeZero_1 = isNegativeZero; +var extend_1 = extend; + +var common = { + isNothing: isNothing_1, + isObject: isObject_1, + toArray: toArray_1, + repeat: repeat_1, + isNegativeZero: isNegativeZero_1, + extend: extend_1 +}; + +// YAML error class. http://stackoverflow.com/questions/8458984 + + +function formatError(exception, compact) { + var where = '', message = exception.reason || '(unknown reason)'; + + if (!exception.mark) return message; + + if (exception.mark.name) { + where += 'in "' + exception.mark.name + '" '; + } + + where += '(' + (exception.mark.line + 1) + ':' + (exception.mark.column + 1) + ')'; + + if (!compact && exception.mark.snippet) { + where += '\n\n' + exception.mark.snippet; + } + + return message + ' ' + where; +} + + +function YAMLException$1(reason, mark) { + // Super constructor + Error.call(this); + + this.name = 'YAMLException'; + this.reason = reason; + this.mark = mark; + this.message = formatError(this, false); + + // Include stack trace in error object + if (Error.captureStackTrace) { + // Chrome and NodeJS + Error.captureStackTrace(this, this.constructor); + } else { + // FF, IE 10+ and Safari 6+. Fallback for others + this.stack = (new Error()).stack || ''; + } +} + + +// Inherit from Error +YAMLException$1.prototype = Object.create(Error.prototype); +YAMLException$1.prototype.constructor = YAMLException$1; + + +YAMLException$1.prototype.toString = function toString(compact) { + return this.name + ': ' + formatError(this, compact); +}; + + +var exception = YAMLException$1; + +// get snippet for a single line, respecting maxLength +function getLine(buffer, lineStart, lineEnd, position, maxLineLength) { + var head = ''; + var tail = ''; + var maxHalfLength = Math.floor(maxLineLength / 2) - 1; + + if (position - lineStart > maxHalfLength) { + head = ' ... '; + lineStart = position - maxHalfLength + head.length; + } + + if (lineEnd - position > maxHalfLength) { + tail = ' ...'; + lineEnd = position + maxHalfLength - tail.length; + } + + return { + str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, '→') + tail, + pos: position - lineStart + head.length // relative position + }; +} + + +function padStart(string, max) { + return common.repeat(' ', max - string.length) + string; +} + + +function makeSnippet(mark, options) { + options = Object.create(options || null); + + if (!mark.buffer) return null; + + if (!options.maxLength) options.maxLength = 79; + if (typeof options.indent !== 'number') options.indent = 1; + if (typeof options.linesBefore !== 'number') options.linesBefore = 3; + if (typeof options.linesAfter !== 'number') options.linesAfter = 2; + + var re = /\r?\n|\r|\0/g; + var lineStarts = [ 0 ]; + var lineEnds = []; + var match; + var foundLineNo = -1; + + while ((match = re.exec(mark.buffer))) { + lineEnds.push(match.index); + lineStarts.push(match.index + match[0].length); + + if (mark.position <= match.index && foundLineNo < 0) { + foundLineNo = lineStarts.length - 2; + } + } + + if (foundLineNo < 0) foundLineNo = lineStarts.length - 1; + + var result = '', i, line; + var lineNoLength = Math.min(mark.line + options.linesAfter, lineEnds.length).toString().length; + var maxLineLength = options.maxLength - (options.indent + lineNoLength + 3); + + for (i = 1; i <= options.linesBefore; i++) { + if (foundLineNo - i < 0) break; + line = getLine( + mark.buffer, + lineStarts[foundLineNo - i], + lineEnds[foundLineNo - i], + mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo - i]), + maxLineLength + ); + result = common.repeat(' ', options.indent) + padStart((mark.line - i + 1).toString(), lineNoLength) + + ' | ' + line.str + '\n' + result; + } + + line = getLine(mark.buffer, lineStarts[foundLineNo], lineEnds[foundLineNo], mark.position, maxLineLength); + result += common.repeat(' ', options.indent) + padStart((mark.line + 1).toString(), lineNoLength) + + ' | ' + line.str + '\n'; + result += common.repeat('-', options.indent + lineNoLength + 3 + line.pos) + '^' + '\n'; + + for (i = 1; i <= options.linesAfter; i++) { + if (foundLineNo + i >= lineEnds.length) break; + line = getLine( + mark.buffer, + lineStarts[foundLineNo + i], + lineEnds[foundLineNo + i], + mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo + i]), + maxLineLength + ); + result += common.repeat(' ', options.indent) + padStart((mark.line + i + 1).toString(), lineNoLength) + + ' | ' + line.str + '\n'; + } + + return result.replace(/\n$/, ''); +} + + +var snippet = makeSnippet; + +var TYPE_CONSTRUCTOR_OPTIONS = [ + 'kind', + 'multi', + 'resolve', + 'construct', + 'instanceOf', + 'predicate', + 'represent', + 'representName', + 'defaultStyle', + 'styleAliases' +]; + +var YAML_NODE_KINDS = [ + 'scalar', + 'sequence', + 'mapping' +]; + +function compileStyleAliases(map) { + var result = {}; + + if (map !== null) { + Object.keys(map).forEach(function (style) { + map[style].forEach(function (alias) { + result[String(alias)] = style; + }); + }); + } + + return result; +} + +function Type$1(tag, options) { + options = options || {}; + + Object.keys(options).forEach(function (name) { + if (TYPE_CONSTRUCTOR_OPTIONS.indexOf(name) === -1) { + throw new exception('Unknown option "' + name + '" is met in definition of "' + tag + '" YAML type.'); + } + }); + + // TODO: Add tag format check. + this.options = options; // keep original options in case user wants to extend this type later + this.tag = tag; + this.kind = options['kind'] || null; + this.resolve = options['resolve'] || function () { return true; }; + this.construct = options['construct'] || function (data) { return data; }; + this.instanceOf = options['instanceOf'] || null; + this.predicate = options['predicate'] || null; + this.represent = options['represent'] || null; + this.representName = options['representName'] || null; + this.defaultStyle = options['defaultStyle'] || null; + this.multi = options['multi'] || false; + this.styleAliases = compileStyleAliases(options['styleAliases'] || null); + + if (YAML_NODE_KINDS.indexOf(this.kind) === -1) { + throw new exception('Unknown kind "' + this.kind + '" is specified for "' + tag + '" YAML type.'); + } +} + +var type = Type$1; + +/*eslint-disable max-len*/ + + + + + +function compileList(schema, name) { + var result = []; + + schema[name].forEach(function (currentType) { + var newIndex = result.length; + + result.forEach(function (previousType, previousIndex) { + if (previousType.tag === currentType.tag && + previousType.kind === currentType.kind && + previousType.multi === currentType.multi) { + + newIndex = previousIndex; + } + }); + + result[newIndex] = currentType; + }); + + return result; +} + + +function compileMap(/* lists... */) { + var result = { + scalar: {}, + sequence: {}, + mapping: {}, + fallback: {}, + multi: { + scalar: [], + sequence: [], + mapping: [], + fallback: [] + } + }, index, length; + + function collectType(type) { + if (type.multi) { + result.multi[type.kind].push(type); + result.multi['fallback'].push(type); + } else { + result[type.kind][type.tag] = result['fallback'][type.tag] = type; + } + } + + for (index = 0, length = arguments.length; index < length; index += 1) { + arguments[index].forEach(collectType); + } + return result; +} + + +function Schema$1(definition) { + return this.extend(definition); +} + + +Schema$1.prototype.extend = function extend(definition) { + var implicit = []; + var explicit = []; + + if (definition instanceof type) { + // Schema.extend(type) + explicit.push(definition); + + } else if (Array.isArray(definition)) { + // Schema.extend([ type1, type2, ... ]) + explicit = explicit.concat(definition); + + } else if (definition && (Array.isArray(definition.implicit) || Array.isArray(definition.explicit))) { + // Schema.extend({ explicit: [ type1, type2, ... ], implicit: [ type1, type2, ... ] }) + if (definition.implicit) implicit = implicit.concat(definition.implicit); + if (definition.explicit) explicit = explicit.concat(definition.explicit); + + } else { + throw new exception('Schema.extend argument should be a Type, [ Type ], ' + + 'or a schema definition ({ implicit: [...], explicit: [...] })'); + } + + implicit.forEach(function (type$1) { + if (!(type$1 instanceof type)) { + throw new exception('Specified list of YAML types (or a single Type object) contains a non-Type object.'); + } + + if (type$1.loadKind && type$1.loadKind !== 'scalar') { + throw new exception('There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.'); + } + + if (type$1.multi) { + throw new exception('There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.'); + } + }); + + explicit.forEach(function (type$1) { + if (!(type$1 instanceof type)) { + throw new exception('Specified list of YAML types (or a single Type object) contains a non-Type object.'); + } + }); + + var result = Object.create(Schema$1.prototype); + + result.implicit = (this.implicit || []).concat(implicit); + result.explicit = (this.explicit || []).concat(explicit); + + result.compiledImplicit = compileList(result, 'implicit'); + result.compiledExplicit = compileList(result, 'explicit'); + result.compiledTypeMap = compileMap(result.compiledImplicit, result.compiledExplicit); + + return result; +}; + + +var schema = Schema$1; + +var str = new type('tag:yaml.org,2002:str', { + kind: 'scalar', + construct: function (data) { return data !== null ? data : ''; } +}); + +var seq = new type('tag:yaml.org,2002:seq', { + kind: 'sequence', + construct: function (data) { return data !== null ? data : []; } +}); + +var map = new type('tag:yaml.org,2002:map', { + kind: 'mapping', + construct: function (data) { return data !== null ? data : {}; } +}); + +var failsafe = new schema({ + explicit: [ + str, + seq, + map + ] +}); + +function resolveYamlNull(data) { + if (data === null) return true; + + var max = data.length; + + return (max === 1 && data === '~') || + (max === 4 && (data === 'null' || data === 'Null' || data === 'NULL')); +} + +function constructYamlNull() { + return null; +} + +function isNull(object) { + return object === null; +} + +var _null = new type('tag:yaml.org,2002:null', { + kind: 'scalar', + resolve: resolveYamlNull, + construct: constructYamlNull, + predicate: isNull, + represent: { + canonical: function () { return '~'; }, + lowercase: function () { return 'null'; }, + uppercase: function () { return 'NULL'; }, + camelcase: function () { return 'Null'; }, + empty: function () { return ''; } + }, + defaultStyle: 'lowercase' +}); + +function resolveYamlBoolean(data) { + if (data === null) return false; + + var max = data.length; + + return (max === 4 && (data === 'true' || data === 'True' || data === 'TRUE')) || + (max === 5 && (data === 'false' || data === 'False' || data === 'FALSE')); +} + +function constructYamlBoolean(data) { + return data === 'true' || + data === 'True' || + data === 'TRUE'; +} + +function isBoolean(object) { + return Object.prototype.toString.call(object) === '[object Boolean]'; +} + +var bool = new type('tag:yaml.org,2002:bool', { + kind: 'scalar', + resolve: resolveYamlBoolean, + construct: constructYamlBoolean, + predicate: isBoolean, + represent: { + lowercase: function (object) { return object ? 'true' : 'false'; }, + uppercase: function (object) { return object ? 'TRUE' : 'FALSE'; }, + camelcase: function (object) { return object ? 'True' : 'False'; } + }, + defaultStyle: 'lowercase' +}); + +function isHexCode(c) { + return ((0x30/* 0 */ <= c) && (c <= 0x39/* 9 */)) || + ((0x41/* A */ <= c) && (c <= 0x46/* F */)) || + ((0x61/* a */ <= c) && (c <= 0x66/* f */)); +} + +function isOctCode(c) { + return ((0x30/* 0 */ <= c) && (c <= 0x37/* 7 */)); +} + +function isDecCode(c) { + return ((0x30/* 0 */ <= c) && (c <= 0x39/* 9 */)); +} + +function resolveYamlInteger(data) { + if (data === null) return false; + + var max = data.length, + index = 0, + hasDigits = false, + ch; + + if (!max) return false; + + ch = data[index]; + + // sign + if (ch === '-' || ch === '+') { + ch = data[++index]; + } + + if (ch === '0') { + // 0 + if (index + 1 === max) return true; + ch = data[++index]; + + // base 2, base 8, base 16 + + if (ch === 'b') { + // base 2 + index++; + + for (; index < max; index++) { + ch = data[index]; + if (ch === '_') continue; + if (ch !== '0' && ch !== '1') return false; + hasDigits = true; + } + return hasDigits && ch !== '_'; + } + + + if (ch === 'x') { + // base 16 + index++; + + for (; index < max; index++) { + ch = data[index]; + if (ch === '_') continue; + if (!isHexCode(data.charCodeAt(index))) return false; + hasDigits = true; + } + return hasDigits && ch !== '_'; + } + + + if (ch === 'o') { + // base 8 + index++; + + for (; index < max; index++) { + ch = data[index]; + if (ch === '_') continue; + if (!isOctCode(data.charCodeAt(index))) return false; + hasDigits = true; + } + return hasDigits && ch !== '_'; + } + } + + // base 10 (except 0) + + // value should not start with `_`; + if (ch === '_') return false; + + for (; index < max; index++) { + ch = data[index]; + if (ch === '_') continue; + if (!isDecCode(data.charCodeAt(index))) { + return false; + } + hasDigits = true; + } + + // Should have digits and should not end with `_` + if (!hasDigits || ch === '_') return false; + + return true; +} + +function constructYamlInteger(data) { + var value = data, sign = 1, ch; + + if (value.indexOf('_') !== -1) { + value = value.replace(/_/g, ''); + } + + ch = value[0]; + + if (ch === '-' || ch === '+') { + if (ch === '-') sign = -1; + value = value.slice(1); + ch = value[0]; + } + + if (value === '0') return 0; + + if (ch === '0') { + if (value[1] === 'b') return sign * parseInt(value.slice(2), 2); + if (value[1] === 'x') return sign * parseInt(value.slice(2), 16); + if (value[1] === 'o') return sign * parseInt(value.slice(2), 8); + } + + return sign * parseInt(value, 10); +} + +function isInteger(object) { + return (Object.prototype.toString.call(object)) === '[object Number]' && + (object % 1 === 0 && !common.isNegativeZero(object)); +} + +var int = new type('tag:yaml.org,2002:int', { + kind: 'scalar', + resolve: resolveYamlInteger, + construct: constructYamlInteger, + predicate: isInteger, + represent: { + binary: function (obj) { return obj >= 0 ? '0b' + obj.toString(2) : '-0b' + obj.toString(2).slice(1); }, + octal: function (obj) { return obj >= 0 ? '0o' + obj.toString(8) : '-0o' + obj.toString(8).slice(1); }, + decimal: function (obj) { return obj.toString(10); }, + /* eslint-disable max-len */ + hexadecimal: function (obj) { return obj >= 0 ? '0x' + obj.toString(16).toUpperCase() : '-0x' + obj.toString(16).toUpperCase().slice(1); } + }, + defaultStyle: 'decimal', + styleAliases: { + binary: [ 2, 'bin' ], + octal: [ 8, 'oct' ], + decimal: [ 10, 'dec' ], + hexadecimal: [ 16, 'hex' ] + } +}); + +var YAML_FLOAT_PATTERN = new RegExp( + // 2.5e4, 2.5 and integers + '^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?' + + // .2e4, .2 + // special case, seems not from spec + '|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?' + + // .inf + '|[-+]?\\.(?:inf|Inf|INF)' + + // .nan + '|\\.(?:nan|NaN|NAN))$'); + +function resolveYamlFloat(data) { + if (data === null) return false; + + if (!YAML_FLOAT_PATTERN.test(data) || + // Quick hack to not allow integers end with `_` + // Probably should update regexp & check speed + data[data.length - 1] === '_') { + return false; + } + + return true; +} + +function constructYamlFloat(data) { + var value, sign; + + value = data.replace(/_/g, '').toLowerCase(); + sign = value[0] === '-' ? -1 : 1; + + if ('+-'.indexOf(value[0]) >= 0) { + value = value.slice(1); + } + + if (value === '.inf') { + return (sign === 1) ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; + + } else if (value === '.nan') { + return NaN; + } + return sign * parseFloat(value, 10); +} + + +var SCIENTIFIC_WITHOUT_DOT = /^[-+]?[0-9]+e/; + +function representYamlFloat(object, style) { + var res; + + if (isNaN(object)) { + switch (style) { + case 'lowercase': return '.nan'; + case 'uppercase': return '.NAN'; + case 'camelcase': return '.NaN'; + } + } else if (Number.POSITIVE_INFINITY === object) { + switch (style) { + case 'lowercase': return '.inf'; + case 'uppercase': return '.INF'; + case 'camelcase': return '.Inf'; + } + } else if (Number.NEGATIVE_INFINITY === object) { + switch (style) { + case 'lowercase': return '-.inf'; + case 'uppercase': return '-.INF'; + case 'camelcase': return '-.Inf'; + } + } else if (common.isNegativeZero(object)) { + return '-0.0'; + } + + res = object.toString(10); + + // JS stringifier can build scientific format without dots: 5e-100, + // while YAML requres dot: 5.e-100. Fix it with simple hack + + return SCIENTIFIC_WITHOUT_DOT.test(res) ? res.replace('e', '.e') : res; +} + +function isFloat(object) { + return (Object.prototype.toString.call(object) === '[object Number]') && + (object % 1 !== 0 || common.isNegativeZero(object)); +} + +var float = new type('tag:yaml.org,2002:float', { + kind: 'scalar', + resolve: resolveYamlFloat, + construct: constructYamlFloat, + predicate: isFloat, + represent: representYamlFloat, + defaultStyle: 'lowercase' +}); + +var json = failsafe.extend({ + implicit: [ + _null, + bool, + int, + float + ] +}); + +var core = json; + +var YAML_DATE_REGEXP = new RegExp( + '^([0-9][0-9][0-9][0-9])' + // [1] year + '-([0-9][0-9])' + // [2] month + '-([0-9][0-9])$'); // [3] day + +var YAML_TIMESTAMP_REGEXP = new RegExp( + '^([0-9][0-9][0-9][0-9])' + // [1] year + '-([0-9][0-9]?)' + // [2] month + '-([0-9][0-9]?)' + // [3] day + '(?:[Tt]|[ \\t]+)' + // ... + '([0-9][0-9]?)' + // [4] hour + ':([0-9][0-9])' + // [5] minute + ':([0-9][0-9])' + // [6] second + '(?:\\.([0-9]*))?' + // [7] fraction + '(?:[ \\t]*(Z|([-+])([0-9][0-9]?)' + // [8] tz [9] tz_sign [10] tz_hour + '(?::([0-9][0-9]))?))?$'); // [11] tz_minute + +function resolveYamlTimestamp(data) { + if (data === null) return false; + if (YAML_DATE_REGEXP.exec(data) !== null) return true; + if (YAML_TIMESTAMP_REGEXP.exec(data) !== null) return true; + return false; +} + +function constructYamlTimestamp(data) { + var match, year, month, day, hour, minute, second, fraction = 0, + delta = null, tz_hour, tz_minute, date; + + match = YAML_DATE_REGEXP.exec(data); + if (match === null) match = YAML_TIMESTAMP_REGEXP.exec(data); + + if (match === null) throw new Error('Date resolve error'); + + // match: [1] year [2] month [3] day + + year = +(match[1]); + month = +(match[2]) - 1; // JS month starts with 0 + day = +(match[3]); + + if (!match[4]) { // no hour + return new Date(Date.UTC(year, month, day)); + } + + // match: [4] hour [5] minute [6] second [7] fraction + + hour = +(match[4]); + minute = +(match[5]); + second = +(match[6]); + + if (match[7]) { + fraction = match[7].slice(0, 3); + while (fraction.length < 3) { // milli-seconds + fraction += '0'; + } + fraction = +fraction; + } + + // match: [8] tz [9] tz_sign [10] tz_hour [11] tz_minute + + if (match[9]) { + tz_hour = +(match[10]); + tz_minute = +(match[11] || 0); + delta = (tz_hour * 60 + tz_minute) * 60000; // delta in mili-seconds + if (match[9] === '-') delta = -delta; + } + + date = new Date(Date.UTC(year, month, day, hour, minute, second, fraction)); + + if (delta) date.setTime(date.getTime() - delta); + + return date; +} + +function representYamlTimestamp(object /*, style*/) { + return object.toISOString(); +} + +var timestamp = new type('tag:yaml.org,2002:timestamp', { + kind: 'scalar', + resolve: resolveYamlTimestamp, + construct: constructYamlTimestamp, + instanceOf: Date, + represent: representYamlTimestamp +}); + +function resolveYamlMerge(data) { + return data === '<<' || data === null; +} + +var merge = new type('tag:yaml.org,2002:merge', { + kind: 'scalar', + resolve: resolveYamlMerge +}); + +/*eslint-disable no-bitwise*/ + + + + + +// [ 64, 65, 66 ] -> [ padding, CR, LF ] +var BASE64_MAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r'; + + +function resolveYamlBinary(data) { + if (data === null) return false; + + var code, idx, bitlen = 0, max = data.length, map = BASE64_MAP; + + // Convert one by one. + for (idx = 0; idx < max; idx++) { + code = map.indexOf(data.charAt(idx)); + + // Skip CR/LF + if (code > 64) continue; + + // Fail on illegal characters + if (code < 0) return false; + + bitlen += 6; + } + + // If there are any bits left, source was corrupted + return (bitlen % 8) === 0; +} + +function constructYamlBinary(data) { + var idx, tailbits, + input = data.replace(/[\r\n=]/g, ''), // remove CR/LF & padding to simplify scan + max = input.length, + map = BASE64_MAP, + bits = 0, + result = []; + + // Collect by 6*4 bits (3 bytes) + + for (idx = 0; idx < max; idx++) { + if ((idx % 4 === 0) && idx) { + result.push((bits >> 16) & 0xFF); + result.push((bits >> 8) & 0xFF); + result.push(bits & 0xFF); + } + + bits = (bits << 6) | map.indexOf(input.charAt(idx)); + } + + // Dump tail + + tailbits = (max % 4) * 6; + + if (tailbits === 0) { + result.push((bits >> 16) & 0xFF); + result.push((bits >> 8) & 0xFF); + result.push(bits & 0xFF); + } else if (tailbits === 18) { + result.push((bits >> 10) & 0xFF); + result.push((bits >> 2) & 0xFF); + } else if (tailbits === 12) { + result.push((bits >> 4) & 0xFF); + } + + return new Uint8Array(result); +} + +function representYamlBinary(object /*, style*/) { + var result = '', bits = 0, idx, tail, + max = object.length, + map = BASE64_MAP; + + // Convert every three bytes to 4 ASCII characters. + + for (idx = 0; idx < max; idx++) { + if ((idx % 3 === 0) && idx) { + result += map[(bits >> 18) & 0x3F]; + result += map[(bits >> 12) & 0x3F]; + result += map[(bits >> 6) & 0x3F]; + result += map[bits & 0x3F]; + } + + bits = (bits << 8) + object[idx]; + } + + // Dump tail + + tail = max % 3; + + if (tail === 0) { + result += map[(bits >> 18) & 0x3F]; + result += map[(bits >> 12) & 0x3F]; + result += map[(bits >> 6) & 0x3F]; + result += map[bits & 0x3F]; + } else if (tail === 2) { + result += map[(bits >> 10) & 0x3F]; + result += map[(bits >> 4) & 0x3F]; + result += map[(bits << 2) & 0x3F]; + result += map[64]; + } else if (tail === 1) { + result += map[(bits >> 2) & 0x3F]; + result += map[(bits << 4) & 0x3F]; + result += map[64]; + result += map[64]; + } + + return result; +} + +function isBinary(obj) { + return Object.prototype.toString.call(obj) === '[object Uint8Array]'; +} + +var binary = new type('tag:yaml.org,2002:binary', { + kind: 'scalar', + resolve: resolveYamlBinary, + construct: constructYamlBinary, + predicate: isBinary, + represent: representYamlBinary +}); + +var _hasOwnProperty$3 = Object.prototype.hasOwnProperty; +var _toString$2 = Object.prototype.toString; + +function resolveYamlOmap(data) { + if (data === null) return true; + + var objectKeys = [], index, length, pair, pairKey, pairHasKey, + object = data; + + for (index = 0, length = object.length; index < length; index += 1) { + pair = object[index]; + pairHasKey = false; + + if (_toString$2.call(pair) !== '[object Object]') return false; + + for (pairKey in pair) { + if (_hasOwnProperty$3.call(pair, pairKey)) { + if (!pairHasKey) pairHasKey = true; + else return false; + } + } + + if (!pairHasKey) return false; + + if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey); + else return false; + } + + return true; +} + +function constructYamlOmap(data) { + return data !== null ? data : []; +} + +var omap = new type('tag:yaml.org,2002:omap', { + kind: 'sequence', + resolve: resolveYamlOmap, + construct: constructYamlOmap +}); + +var _toString$1 = Object.prototype.toString; + +function resolveYamlPairs(data) { + if (data === null) return true; + + var index, length, pair, keys, result, + object = data; + + result = new Array(object.length); + + for (index = 0, length = object.length; index < length; index += 1) { + pair = object[index]; + + if (_toString$1.call(pair) !== '[object Object]') return false; + + keys = Object.keys(pair); + + if (keys.length !== 1) return false; + + result[index] = [ keys[0], pair[keys[0]] ]; + } + + return true; +} + +function constructYamlPairs(data) { + if (data === null) return []; + + var index, length, pair, keys, result, + object = data; + + result = new Array(object.length); + + for (index = 0, length = object.length; index < length; index += 1) { + pair = object[index]; + + keys = Object.keys(pair); + + result[index] = [ keys[0], pair[keys[0]] ]; + } + + return result; +} + +var pairs = new type('tag:yaml.org,2002:pairs', { + kind: 'sequence', + resolve: resolveYamlPairs, + construct: constructYamlPairs +}); + +var _hasOwnProperty$2 = Object.prototype.hasOwnProperty; + +function resolveYamlSet(data) { + if (data === null) return true; + + var key, object = data; + + for (key in object) { + if (_hasOwnProperty$2.call(object, key)) { + if (object[key] !== null) return false; + } + } + + return true; +} + +function constructYamlSet(data) { + return data !== null ? data : {}; +} + +var set = new type('tag:yaml.org,2002:set', { + kind: 'mapping', + resolve: resolveYamlSet, + construct: constructYamlSet +}); + +var _default = core.extend({ + implicit: [ + timestamp, + merge + ], + explicit: [ + binary, + omap, + pairs, + set + ] +}); + +/*eslint-disable max-len,no-use-before-define*/ + + + + + + + +var _hasOwnProperty$1 = Object.prototype.hasOwnProperty; + + +var CONTEXT_FLOW_IN = 1; +var CONTEXT_FLOW_OUT = 2; +var CONTEXT_BLOCK_IN = 3; +var CONTEXT_BLOCK_OUT = 4; + + +var CHOMPING_CLIP = 1; +var CHOMPING_STRIP = 2; +var CHOMPING_KEEP = 3; + + +var PATTERN_NON_PRINTABLE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/; +var PATTERN_NON_ASCII_LINE_BREAKS = /[\x85\u2028\u2029]/; +var PATTERN_FLOW_INDICATORS = /[,\[\]\{\}]/; +var PATTERN_TAG_HANDLE = /^(?:!|!!|![a-z\-]+!)$/i; +var PATTERN_TAG_URI = /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i; + + +function _class(obj) { return Object.prototype.toString.call(obj); } + +function is_EOL(c) { + return (c === 0x0A/* LF */) || (c === 0x0D/* CR */); +} + +function is_WHITE_SPACE(c) { + return (c === 0x09/* Tab */) || (c === 0x20/* Space */); +} + +function is_WS_OR_EOL(c) { + return (c === 0x09/* Tab */) || + (c === 0x20/* Space */) || + (c === 0x0A/* LF */) || + (c === 0x0D/* CR */); +} + +function is_FLOW_INDICATOR(c) { + return c === 0x2C/* , */ || + c === 0x5B/* [ */ || + c === 0x5D/* ] */ || + c === 0x7B/* { */ || + c === 0x7D/* } */; +} + +function fromHexCode(c) { + var lc; + + if ((0x30/* 0 */ <= c) && (c <= 0x39/* 9 */)) { + return c - 0x30; + } + + /*eslint-disable no-bitwise*/ + lc = c | 0x20; + + if ((0x61/* a */ <= lc) && (lc <= 0x66/* f */)) { + return lc - 0x61 + 10; + } + + return -1; +} + +function escapedHexLen(c) { + if (c === 0x78/* x */) { return 2; } + if (c === 0x75/* u */) { return 4; } + if (c === 0x55/* U */) { return 8; } + return 0; +} + +function fromDecimalCode(c) { + if ((0x30/* 0 */ <= c) && (c <= 0x39/* 9 */)) { + return c - 0x30; + } + + return -1; +} + +function simpleEscapeSequence(c) { + /* eslint-disable indent */ + return (c === 0x30/* 0 */) ? '\x00' : + (c === 0x61/* a */) ? '\x07' : + (c === 0x62/* b */) ? '\x08' : + (c === 0x74/* t */) ? '\x09' : + (c === 0x09/* Tab */) ? '\x09' : + (c === 0x6E/* n */) ? '\x0A' : + (c === 0x76/* v */) ? '\x0B' : + (c === 0x66/* f */) ? '\x0C' : + (c === 0x72/* r */) ? '\x0D' : + (c === 0x65/* e */) ? '\x1B' : + (c === 0x20/* Space */) ? ' ' : + (c === 0x22/* " */) ? '\x22' : + (c === 0x2F/* / */) ? '/' : + (c === 0x5C/* \ */) ? '\x5C' : + (c === 0x4E/* N */) ? '\x85' : + (c === 0x5F/* _ */) ? '\xA0' : + (c === 0x4C/* L */) ? '\u2028' : + (c === 0x50/* P */) ? '\u2029' : ''; +} + +function charFromCodepoint(c) { + if (c <= 0xFFFF) { + return String.fromCharCode(c); + } + // Encode UTF-16 surrogate pair + // https://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B010000_to_U.2B10FFFF + return String.fromCharCode( + ((c - 0x010000) >> 10) + 0xD800, + ((c - 0x010000) & 0x03FF) + 0xDC00 + ); +} + +var simpleEscapeCheck = new Array(256); // integer, for fast access +var simpleEscapeMap = new Array(256); +for (var i = 0; i < 256; i++) { + simpleEscapeCheck[i] = simpleEscapeSequence(i) ? 1 : 0; + simpleEscapeMap[i] = simpleEscapeSequence(i); +} + + +function State$1(input, options) { + this.input = input; + + this.filename = options['filename'] || null; + this.schema = options['schema'] || _default; + this.onWarning = options['onWarning'] || null; + // (Hidden) Remove? makes the loader to expect YAML 1.1 documents + // if such documents have no explicit %YAML directive + this.legacy = options['legacy'] || false; + + this.json = options['json'] || false; + this.listener = options['listener'] || null; + + this.implicitTypes = this.schema.compiledImplicit; + this.typeMap = this.schema.compiledTypeMap; + + this.length = input.length; + this.position = 0; + this.line = 0; + this.lineStart = 0; + this.lineIndent = 0; + + // position of first leading tab in the current line, + // used to make sure there are no tabs in the indentation + this.firstTabInLine = -1; + + this.documents = []; + + /* + this.version; + this.checkLineBreaks; + this.tagMap; + this.anchorMap; + this.tag; + this.anchor; + this.kind; + this.result;*/ + +} + + +function generateError(state, message) { + var mark = { + name: state.filename, + buffer: state.input.slice(0, -1), // omit trailing \0 + position: state.position, + line: state.line, + column: state.position - state.lineStart + }; + + mark.snippet = snippet(mark); + + return new exception(message, mark); +} + +function throwError(state, message) { + throw generateError(state, message); +} + +function throwWarning(state, message) { + if (state.onWarning) { + state.onWarning.call(null, generateError(state, message)); + } +} + + +var directiveHandlers = { + + YAML: function handleYamlDirective(state, name, args) { + + var match, major, minor; + + if (state.version !== null) { + throwError(state, 'duplication of %YAML directive'); + } + + if (args.length !== 1) { + throwError(state, 'YAML directive accepts exactly one argument'); + } + + match = /^([0-9]+)\.([0-9]+)$/.exec(args[0]); + + if (match === null) { + throwError(state, 'ill-formed argument of the YAML directive'); + } + + major = parseInt(match[1], 10); + minor = parseInt(match[2], 10); + + if (major !== 1) { + throwError(state, 'unacceptable YAML version of the document'); + } + + state.version = args[0]; + state.checkLineBreaks = (minor < 2); + + if (minor !== 1 && minor !== 2) { + throwWarning(state, 'unsupported YAML version of the document'); + } + }, + + TAG: function handleTagDirective(state, name, args) { + + var handle, prefix; + + if (args.length !== 2) { + throwError(state, 'TAG directive accepts exactly two arguments'); + } + + handle = args[0]; + prefix = args[1]; + + if (!PATTERN_TAG_HANDLE.test(handle)) { + throwError(state, 'ill-formed tag handle (first argument) of the TAG directive'); + } + + if (_hasOwnProperty$1.call(state.tagMap, handle)) { + throwError(state, 'there is a previously declared suffix for "' + handle + '" tag handle'); + } + + if (!PATTERN_TAG_URI.test(prefix)) { + throwError(state, 'ill-formed tag prefix (second argument) of the TAG directive'); + } + + try { + prefix = decodeURIComponent(prefix); + } catch (err) { + throwError(state, 'tag prefix is malformed: ' + prefix); + } + + state.tagMap[handle] = prefix; + } +}; + + +function captureSegment(state, start, end, checkJson) { + var _position, _length, _character, _result; + + if (start < end) { + _result = state.input.slice(start, end); + + if (checkJson) { + for (_position = 0, _length = _result.length; _position < _length; _position += 1) { + _character = _result.charCodeAt(_position); + if (!(_character === 0x09 || + (0x20 <= _character && _character <= 0x10FFFF))) { + throwError(state, 'expected valid JSON character'); + } + } + } else if (PATTERN_NON_PRINTABLE.test(_result)) { + throwError(state, 'the stream contains non-printable characters'); + } + + state.result += _result; + } +} + +function mergeMappings(state, destination, source, overridableKeys) { + var sourceKeys, key, index, quantity; + + if (!common.isObject(source)) { + throwError(state, 'cannot merge mappings; the provided source object is unacceptable'); + } + + sourceKeys = Object.keys(source); + + for (index = 0, quantity = sourceKeys.length; index < quantity; index += 1) { + key = sourceKeys[index]; + + if (!_hasOwnProperty$1.call(destination, key)) { + destination[key] = source[key]; + overridableKeys[key] = true; + } + } +} + +function storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valueNode, + startLine, startLineStart, startPos) { + + var index, quantity; + + // The output is a plain object here, so keys can only be strings. + // We need to convert keyNode to a string, but doing so can hang the process + // (deeply nested arrays that explode exponentially using aliases). + if (Array.isArray(keyNode)) { + keyNode = Array.prototype.slice.call(keyNode); + + for (index = 0, quantity = keyNode.length; index < quantity; index += 1) { + if (Array.isArray(keyNode[index])) { + throwError(state, 'nested arrays are not supported inside keys'); + } + + if (typeof keyNode === 'object' && _class(keyNode[index]) === '[object Object]') { + keyNode[index] = '[object Object]'; + } + } + } + + // Avoid code execution in load() via toString property + // (still use its own toString for arrays, timestamps, + // and whatever user schema extensions happen to have @@toStringTag) + if (typeof keyNode === 'object' && _class(keyNode) === '[object Object]') { + keyNode = '[object Object]'; + } + + + keyNode = String(keyNode); + + if (_result === null) { + _result = {}; + } + + if (keyTag === 'tag:yaml.org,2002:merge') { + if (Array.isArray(valueNode)) { + for (index = 0, quantity = valueNode.length; index < quantity; index += 1) { + mergeMappings(state, _result, valueNode[index], overridableKeys); + } + } else { + mergeMappings(state, _result, valueNode, overridableKeys); + } + } else { + if (!state.json && + !_hasOwnProperty$1.call(overridableKeys, keyNode) && + _hasOwnProperty$1.call(_result, keyNode)) { + state.line = startLine || state.line; + state.lineStart = startLineStart || state.lineStart; + state.position = startPos || state.position; + throwError(state, 'duplicated mapping key'); + } + + // used for this specific key only because Object.defineProperty is slow + if (keyNode === '__proto__') { + Object.defineProperty(_result, keyNode, { + configurable: true, + enumerable: true, + writable: true, + value: valueNode + }); + } else { + _result[keyNode] = valueNode; + } + delete overridableKeys[keyNode]; + } + + return _result; +} + +function readLineBreak(state) { + var ch; + + ch = state.input.charCodeAt(state.position); + + if (ch === 0x0A/* LF */) { + state.position++; + } else if (ch === 0x0D/* CR */) { + state.position++; + if (state.input.charCodeAt(state.position) === 0x0A/* LF */) { + state.position++; + } + } else { + throwError(state, 'a line break is expected'); + } + + state.line += 1; + state.lineStart = state.position; + state.firstTabInLine = -1; +} + +function skipSeparationSpace(state, allowComments, checkIndent) { + var lineBreaks = 0, + ch = state.input.charCodeAt(state.position); + + while (ch !== 0) { + while (is_WHITE_SPACE(ch)) { + if (ch === 0x09/* Tab */ && state.firstTabInLine === -1) { + state.firstTabInLine = state.position; + } + ch = state.input.charCodeAt(++state.position); + } + + if (allowComments && ch === 0x23/* # */) { + do { + ch = state.input.charCodeAt(++state.position); + } while (ch !== 0x0A/* LF */ && ch !== 0x0D/* CR */ && ch !== 0); + } + + if (is_EOL(ch)) { + readLineBreak(state); + + ch = state.input.charCodeAt(state.position); + lineBreaks++; + state.lineIndent = 0; + + while (ch === 0x20/* Space */) { + state.lineIndent++; + ch = state.input.charCodeAt(++state.position); + } + } else { + break; + } + } + + if (checkIndent !== -1 && lineBreaks !== 0 && state.lineIndent < checkIndent) { + throwWarning(state, 'deficient indentation'); + } + + return lineBreaks; +} + +function testDocumentSeparator(state) { + var _position = state.position, + ch; + + ch = state.input.charCodeAt(_position); + + // Condition state.position === state.lineStart is tested + // in parent on each call, for efficiency. No needs to test here again. + if ((ch === 0x2D/* - */ || ch === 0x2E/* . */) && + ch === state.input.charCodeAt(_position + 1) && + ch === state.input.charCodeAt(_position + 2)) { + + _position += 3; + + ch = state.input.charCodeAt(_position); + + if (ch === 0 || is_WS_OR_EOL(ch)) { + return true; + } + } + + return false; +} + +function writeFoldedLines(state, count) { + if (count === 1) { + state.result += ' '; + } else if (count > 1) { + state.result += common.repeat('\n', count - 1); + } +} + + +function readPlainScalar(state, nodeIndent, withinFlowCollection) { + var preceding, + following, + captureStart, + captureEnd, + hasPendingContent, + _line, + _lineStart, + _lineIndent, + _kind = state.kind, + _result = state.result, + ch; + + ch = state.input.charCodeAt(state.position); + + if (is_WS_OR_EOL(ch) || + is_FLOW_INDICATOR(ch) || + ch === 0x23/* # */ || + ch === 0x26/* & */ || + ch === 0x2A/* * */ || + ch === 0x21/* ! */ || + ch === 0x7C/* | */ || + ch === 0x3E/* > */ || + ch === 0x27/* ' */ || + ch === 0x22/* " */ || + ch === 0x25/* % */ || + ch === 0x40/* @ */ || + ch === 0x60/* ` */) { + return false; + } + + if (ch === 0x3F/* ? */ || ch === 0x2D/* - */) { + following = state.input.charCodeAt(state.position + 1); + + if (is_WS_OR_EOL(following) || + withinFlowCollection && is_FLOW_INDICATOR(following)) { + return false; + } + } + + state.kind = 'scalar'; + state.result = ''; + captureStart = captureEnd = state.position; + hasPendingContent = false; + + while (ch !== 0) { + if (ch === 0x3A/* : */) { + following = state.input.charCodeAt(state.position + 1); + + if (is_WS_OR_EOL(following) || + withinFlowCollection && is_FLOW_INDICATOR(following)) { + break; + } + + } else if (ch === 0x23/* # */) { + preceding = state.input.charCodeAt(state.position - 1); + + if (is_WS_OR_EOL(preceding)) { + break; + } + + } else if ((state.position === state.lineStart && testDocumentSeparator(state)) || + withinFlowCollection && is_FLOW_INDICATOR(ch)) { + break; + + } else if (is_EOL(ch)) { + _line = state.line; + _lineStart = state.lineStart; + _lineIndent = state.lineIndent; + skipSeparationSpace(state, false, -1); + + if (state.lineIndent >= nodeIndent) { + hasPendingContent = true; + ch = state.input.charCodeAt(state.position); + continue; + } else { + state.position = captureEnd; + state.line = _line; + state.lineStart = _lineStart; + state.lineIndent = _lineIndent; + break; + } + } + + if (hasPendingContent) { + captureSegment(state, captureStart, captureEnd, false); + writeFoldedLines(state, state.line - _line); + captureStart = captureEnd = state.position; + hasPendingContent = false; + } + + if (!is_WHITE_SPACE(ch)) { + captureEnd = state.position + 1; + } + + ch = state.input.charCodeAt(++state.position); + } + + captureSegment(state, captureStart, captureEnd, false); + + if (state.result) { + return true; + } + + state.kind = _kind; + state.result = _result; + return false; +} + +function readSingleQuotedScalar(state, nodeIndent) { + var ch, + captureStart, captureEnd; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x27/* ' */) { + return false; + } + + state.kind = 'scalar'; + state.result = ''; + state.position++; + captureStart = captureEnd = state.position; + + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + if (ch === 0x27/* ' */) { + captureSegment(state, captureStart, state.position, true); + ch = state.input.charCodeAt(++state.position); + + if (ch === 0x27/* ' */) { + captureStart = state.position; + state.position++; + captureEnd = state.position; + } else { + return true; + } + + } else if (is_EOL(ch)) { + captureSegment(state, captureStart, captureEnd, true); + writeFoldedLines(state, skipSeparationSpace(state, false, nodeIndent)); + captureStart = captureEnd = state.position; + + } else if (state.position === state.lineStart && testDocumentSeparator(state)) { + throwError(state, 'unexpected end of the document within a single quoted scalar'); + + } else { + state.position++; + captureEnd = state.position; + } + } + + throwError(state, 'unexpected end of the stream within a single quoted scalar'); +} + +function readDoubleQuotedScalar(state, nodeIndent) { + var captureStart, + captureEnd, + hexLength, + hexResult, + tmp, + ch; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x22/* " */) { + return false; + } + + state.kind = 'scalar'; + state.result = ''; + state.position++; + captureStart = captureEnd = state.position; + + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + if (ch === 0x22/* " */) { + captureSegment(state, captureStart, state.position, true); + state.position++; + return true; + + } else if (ch === 0x5C/* \ */) { + captureSegment(state, captureStart, state.position, true); + ch = state.input.charCodeAt(++state.position); + + if (is_EOL(ch)) { + skipSeparationSpace(state, false, nodeIndent); + + // TODO: rework to inline fn with no type cast? + } else if (ch < 256 && simpleEscapeCheck[ch]) { + state.result += simpleEscapeMap[ch]; + state.position++; + + } else if ((tmp = escapedHexLen(ch)) > 0) { + hexLength = tmp; + hexResult = 0; + + for (; hexLength > 0; hexLength--) { + ch = state.input.charCodeAt(++state.position); + + if ((tmp = fromHexCode(ch)) >= 0) { + hexResult = (hexResult << 4) + tmp; + + } else { + throwError(state, 'expected hexadecimal character'); + } + } + + state.result += charFromCodepoint(hexResult); + + state.position++; + + } else { + throwError(state, 'unknown escape sequence'); + } + + captureStart = captureEnd = state.position; + + } else if (is_EOL(ch)) { + captureSegment(state, captureStart, captureEnd, true); + writeFoldedLines(state, skipSeparationSpace(state, false, nodeIndent)); + captureStart = captureEnd = state.position; + + } else if (state.position === state.lineStart && testDocumentSeparator(state)) { + throwError(state, 'unexpected end of the document within a double quoted scalar'); + + } else { + state.position++; + captureEnd = state.position; + } + } + + throwError(state, 'unexpected end of the stream within a double quoted scalar'); +} + +function readFlowCollection(state, nodeIndent) { + var readNext = true, + _line, + _lineStart, + _pos, + _tag = state.tag, + _result, + _anchor = state.anchor, + following, + terminator, + isPair, + isExplicitPair, + isMapping, + overridableKeys = Object.create(null), + keyNode, + keyTag, + valueNode, + ch; + + ch = state.input.charCodeAt(state.position); + + if (ch === 0x5B/* [ */) { + terminator = 0x5D;/* ] */ + isMapping = false; + _result = []; + } else if (ch === 0x7B/* { */) { + terminator = 0x7D;/* } */ + isMapping = true; + _result = {}; + } else { + return false; + } + + if (state.anchor !== null) { + state.anchorMap[state.anchor] = _result; + } + + ch = state.input.charCodeAt(++state.position); + + while (ch !== 0) { + skipSeparationSpace(state, true, nodeIndent); + + ch = state.input.charCodeAt(state.position); + + if (ch === terminator) { + state.position++; + state.tag = _tag; + state.anchor = _anchor; + state.kind = isMapping ? 'mapping' : 'sequence'; + state.result = _result; + return true; + } else if (!readNext) { + throwError(state, 'missed comma between flow collection entries'); + } else if (ch === 0x2C/* , */) { + // "flow collection entries can never be completely empty", as per YAML 1.2, section 7.4 + throwError(state, "expected the node content, but found ','"); + } + + keyTag = keyNode = valueNode = null; + isPair = isExplicitPair = false; + + if (ch === 0x3F/* ? */) { + following = state.input.charCodeAt(state.position + 1); + + if (is_WS_OR_EOL(following)) { + isPair = isExplicitPair = true; + state.position++; + skipSeparationSpace(state, true, nodeIndent); + } + } + + _line = state.line; // Save the current line. + _lineStart = state.lineStart; + _pos = state.position; + composeNode(state, nodeIndent, CONTEXT_FLOW_IN, false, true); + keyTag = state.tag; + keyNode = state.result; + skipSeparationSpace(state, true, nodeIndent); + + ch = state.input.charCodeAt(state.position); + + if ((isExplicitPair || state.line === _line) && ch === 0x3A/* : */) { + isPair = true; + ch = state.input.charCodeAt(++state.position); + skipSeparationSpace(state, true, nodeIndent); + composeNode(state, nodeIndent, CONTEXT_FLOW_IN, false, true); + valueNode = state.result; + } + + if (isMapping) { + storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valueNode, _line, _lineStart, _pos); + } else if (isPair) { + _result.push(storeMappingPair(state, null, overridableKeys, keyTag, keyNode, valueNode, _line, _lineStart, _pos)); + } else { + _result.push(keyNode); + } + + skipSeparationSpace(state, true, nodeIndent); + + ch = state.input.charCodeAt(state.position); + + if (ch === 0x2C/* , */) { + readNext = true; + ch = state.input.charCodeAt(++state.position); + } else { + readNext = false; + } + } + + throwError(state, 'unexpected end of the stream within a flow collection'); +} + +function readBlockScalar(state, nodeIndent) { + var captureStart, + folding, + chomping = CHOMPING_CLIP, + didReadContent = false, + detectedIndent = false, + textIndent = nodeIndent, + emptyLines = 0, + atMoreIndented = false, + tmp, + ch; + + ch = state.input.charCodeAt(state.position); + + if (ch === 0x7C/* | */) { + folding = false; + } else if (ch === 0x3E/* > */) { + folding = true; + } else { + return false; + } + + state.kind = 'scalar'; + state.result = ''; + + while (ch !== 0) { + ch = state.input.charCodeAt(++state.position); + + if (ch === 0x2B/* + */ || ch === 0x2D/* - */) { + if (CHOMPING_CLIP === chomping) { + chomping = (ch === 0x2B/* + */) ? CHOMPING_KEEP : CHOMPING_STRIP; + } else { + throwError(state, 'repeat of a chomping mode identifier'); + } + + } else if ((tmp = fromDecimalCode(ch)) >= 0) { + if (tmp === 0) { + throwError(state, 'bad explicit indentation width of a block scalar; it cannot be less than one'); + } else if (!detectedIndent) { + textIndent = nodeIndent + tmp - 1; + detectedIndent = true; + } else { + throwError(state, 'repeat of an indentation width identifier'); + } + + } else { + break; + } + } + + if (is_WHITE_SPACE(ch)) { + do { ch = state.input.charCodeAt(++state.position); } + while (is_WHITE_SPACE(ch)); + + if (ch === 0x23/* # */) { + do { ch = state.input.charCodeAt(++state.position); } + while (!is_EOL(ch) && (ch !== 0)); + } + } + + while (ch !== 0) { + readLineBreak(state); + state.lineIndent = 0; + + ch = state.input.charCodeAt(state.position); + + while ((!detectedIndent || state.lineIndent < textIndent) && + (ch === 0x20/* Space */)) { + state.lineIndent++; + ch = state.input.charCodeAt(++state.position); + } + + if (!detectedIndent && state.lineIndent > textIndent) { + textIndent = state.lineIndent; + } + + if (is_EOL(ch)) { + emptyLines++; + continue; + } + + // End of the scalar. + if (state.lineIndent < textIndent) { + + // Perform the chomping. + if (chomping === CHOMPING_KEEP) { + state.result += common.repeat('\n', didReadContent ? 1 + emptyLines : emptyLines); + } else if (chomping === CHOMPING_CLIP) { + if (didReadContent) { // i.e. only if the scalar is not empty. + state.result += '\n'; + } + } + + // Break this `while` cycle and go to the funciton's epilogue. + break; + } + + // Folded style: use fancy rules to handle line breaks. + if (folding) { + + // Lines starting with white space characters (more-indented lines) are not folded. + if (is_WHITE_SPACE(ch)) { + atMoreIndented = true; + // except for the first content line (cf. Example 8.1) + state.result += common.repeat('\n', didReadContent ? 1 + emptyLines : emptyLines); + + // End of more-indented block. + } else if (atMoreIndented) { + atMoreIndented = false; + state.result += common.repeat('\n', emptyLines + 1); + + // Just one line break - perceive as the same line. + } else if (emptyLines === 0) { + if (didReadContent) { // i.e. only if we have already read some scalar content. + state.result += ' '; + } + + // Several line breaks - perceive as different lines. + } else { + state.result += common.repeat('\n', emptyLines); + } + + // Literal style: just add exact number of line breaks between content lines. + } else { + // Keep all line breaks except the header line break. + state.result += common.repeat('\n', didReadContent ? 1 + emptyLines : emptyLines); + } + + didReadContent = true; + detectedIndent = true; + emptyLines = 0; + captureStart = state.position; + + while (!is_EOL(ch) && (ch !== 0)) { + ch = state.input.charCodeAt(++state.position); + } + + captureSegment(state, captureStart, state.position, false); + } + + return true; +} + +function readBlockSequence(state, nodeIndent) { + var _line, + _tag = state.tag, + _anchor = state.anchor, + _result = [], + following, + detected = false, + ch; + + // there is a leading tab before this token, so it can't be a block sequence/mapping; + // it can still be flow sequence/mapping or a scalar + if (state.firstTabInLine !== -1) return false; + + if (state.anchor !== null) { + state.anchorMap[state.anchor] = _result; + } + + ch = state.input.charCodeAt(state.position); + + while (ch !== 0) { + if (state.firstTabInLine !== -1) { + state.position = state.firstTabInLine; + throwError(state, 'tab characters must not be used in indentation'); + } + + if (ch !== 0x2D/* - */) { + break; + } + + following = state.input.charCodeAt(state.position + 1); + + if (!is_WS_OR_EOL(following)) { + break; + } + + detected = true; + state.position++; + + if (skipSeparationSpace(state, true, -1)) { + if (state.lineIndent <= nodeIndent) { + _result.push(null); + ch = state.input.charCodeAt(state.position); + continue; + } + } + + _line = state.line; + composeNode(state, nodeIndent, CONTEXT_BLOCK_IN, false, true); + _result.push(state.result); + skipSeparationSpace(state, true, -1); + + ch = state.input.charCodeAt(state.position); + + if ((state.line === _line || state.lineIndent > nodeIndent) && (ch !== 0)) { + throwError(state, 'bad indentation of a sequence entry'); + } else if (state.lineIndent < nodeIndent) { + break; + } + } + + if (detected) { + state.tag = _tag; + state.anchor = _anchor; + state.kind = 'sequence'; + state.result = _result; + return true; + } + return false; +} + +function readBlockMapping(state, nodeIndent, flowIndent) { + var following, + allowCompact, + _line, + _keyLine, + _keyLineStart, + _keyPos, + _tag = state.tag, + _anchor = state.anchor, + _result = {}, + overridableKeys = Object.create(null), + keyTag = null, + keyNode = null, + valueNode = null, + atExplicitKey = false, + detected = false, + ch; + + // there is a leading tab before this token, so it can't be a block sequence/mapping; + // it can still be flow sequence/mapping or a scalar + if (state.firstTabInLine !== -1) return false; + + if (state.anchor !== null) { + state.anchorMap[state.anchor] = _result; + } + + ch = state.input.charCodeAt(state.position); + + while (ch !== 0) { + if (!atExplicitKey && state.firstTabInLine !== -1) { + state.position = state.firstTabInLine; + throwError(state, 'tab characters must not be used in indentation'); + } + + following = state.input.charCodeAt(state.position + 1); + _line = state.line; // Save the current line. + + // + // Explicit notation case. There are two separate blocks: + // first for the key (denoted by "?") and second for the value (denoted by ":") + // + if ((ch === 0x3F/* ? */ || ch === 0x3A/* : */) && is_WS_OR_EOL(following)) { + + if (ch === 0x3F/* ? */) { + if (atExplicitKey) { + storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, null, _keyLine, _keyLineStart, _keyPos); + keyTag = keyNode = valueNode = null; + } + + detected = true; + atExplicitKey = true; + allowCompact = true; + + } else if (atExplicitKey) { + // i.e. 0x3A/* : */ === character after the explicit key. + atExplicitKey = false; + allowCompact = true; + + } else { + throwError(state, 'incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line'); + } + + state.position += 1; + ch = following; + + // + // Implicit notation case. Flow-style node as the key first, then ":", and the value. + // + } else { + _keyLine = state.line; + _keyLineStart = state.lineStart; + _keyPos = state.position; + + if (!composeNode(state, flowIndent, CONTEXT_FLOW_OUT, false, true)) { + // Neither implicit nor explicit notation. + // Reading is done. Go to the epilogue. + break; + } + + if (state.line === _line) { + ch = state.input.charCodeAt(state.position); + + while (is_WHITE_SPACE(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (ch === 0x3A/* : */) { + ch = state.input.charCodeAt(++state.position); + + if (!is_WS_OR_EOL(ch)) { + throwError(state, 'a whitespace character is expected after the key-value separator within a block mapping'); + } + + if (atExplicitKey) { + storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, null, _keyLine, _keyLineStart, _keyPos); + keyTag = keyNode = valueNode = null; + } + + detected = true; + atExplicitKey = false; + allowCompact = false; + keyTag = state.tag; + keyNode = state.result; + + } else if (detected) { + throwError(state, 'can not read an implicit mapping pair; a colon is missed'); + + } else { + state.tag = _tag; + state.anchor = _anchor; + return true; // Keep the result of `composeNode`. + } + + } else if (detected) { + throwError(state, 'can not read a block mapping entry; a multiline key may not be an implicit key'); + + } else { + state.tag = _tag; + state.anchor = _anchor; + return true; // Keep the result of `composeNode`. + } + } + + // + // Common reading code for both explicit and implicit notations. + // + if (state.line === _line || state.lineIndent > nodeIndent) { + if (atExplicitKey) { + _keyLine = state.line; + _keyLineStart = state.lineStart; + _keyPos = state.position; + } + + if (composeNode(state, nodeIndent, CONTEXT_BLOCK_OUT, true, allowCompact)) { + if (atExplicitKey) { + keyNode = state.result; + } else { + valueNode = state.result; + } + } + + if (!atExplicitKey) { + storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valueNode, _keyLine, _keyLineStart, _keyPos); + keyTag = keyNode = valueNode = null; + } + + skipSeparationSpace(state, true, -1); + ch = state.input.charCodeAt(state.position); + } + + if ((state.line === _line || state.lineIndent > nodeIndent) && (ch !== 0)) { + throwError(state, 'bad indentation of a mapping entry'); + } else if (state.lineIndent < nodeIndent) { + break; + } + } + + // + // Epilogue. + // + + // Special case: last mapping's node contains only the key in explicit notation. + if (atExplicitKey) { + storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, null, _keyLine, _keyLineStart, _keyPos); + } + + // Expose the resulting mapping. + if (detected) { + state.tag = _tag; + state.anchor = _anchor; + state.kind = 'mapping'; + state.result = _result; + } + + return detected; +} + +function readTagProperty(state) { + var _position, + isVerbatim = false, + isNamed = false, + tagHandle, + tagName, + ch; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x21/* ! */) return false; + + if (state.tag !== null) { + throwError(state, 'duplication of a tag property'); + } + + ch = state.input.charCodeAt(++state.position); + + if (ch === 0x3C/* < */) { + isVerbatim = true; + ch = state.input.charCodeAt(++state.position); + + } else if (ch === 0x21/* ! */) { + isNamed = true; + tagHandle = '!!'; + ch = state.input.charCodeAt(++state.position); + + } else { + tagHandle = '!'; + } + + _position = state.position; + + if (isVerbatim) { + do { ch = state.input.charCodeAt(++state.position); } + while (ch !== 0 && ch !== 0x3E/* > */); + + if (state.position < state.length) { + tagName = state.input.slice(_position, state.position); + ch = state.input.charCodeAt(++state.position); + } else { + throwError(state, 'unexpected end of the stream within a verbatim tag'); + } + } else { + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + + if (ch === 0x21/* ! */) { + if (!isNamed) { + tagHandle = state.input.slice(_position - 1, state.position + 1); + + if (!PATTERN_TAG_HANDLE.test(tagHandle)) { + throwError(state, 'named tag handle cannot contain such characters'); + } + + isNamed = true; + _position = state.position + 1; + } else { + throwError(state, 'tag suffix cannot contain exclamation marks'); + } + } + + ch = state.input.charCodeAt(++state.position); + } + + tagName = state.input.slice(_position, state.position); + + if (PATTERN_FLOW_INDICATORS.test(tagName)) { + throwError(state, 'tag suffix cannot contain flow indicator characters'); + } + } + + if (tagName && !PATTERN_TAG_URI.test(tagName)) { + throwError(state, 'tag name cannot contain such characters: ' + tagName); + } + + try { + tagName = decodeURIComponent(tagName); + } catch (err) { + throwError(state, 'tag name is malformed: ' + tagName); + } + + if (isVerbatim) { + state.tag = tagName; + + } else if (_hasOwnProperty$1.call(state.tagMap, tagHandle)) { + state.tag = state.tagMap[tagHandle] + tagName; + + } else if (tagHandle === '!') { + state.tag = '!' + tagName; + + } else if (tagHandle === '!!') { + state.tag = 'tag:yaml.org,2002:' + tagName; + + } else { + throwError(state, 'undeclared tag handle "' + tagHandle + '"'); + } + + return true; +} + +function readAnchorProperty(state) { + var _position, + ch; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x26/* & */) return false; + + if (state.anchor !== null) { + throwError(state, 'duplication of an anchor property'); + } + + ch = state.input.charCodeAt(++state.position); + _position = state.position; + + while (ch !== 0 && !is_WS_OR_EOL(ch) && !is_FLOW_INDICATOR(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (state.position === _position) { + throwError(state, 'name of an anchor node must contain at least one character'); + } + + state.anchor = state.input.slice(_position, state.position); + return true; +} + +function readAlias(state) { + var _position, alias, + ch; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x2A/* * */) return false; + + ch = state.input.charCodeAt(++state.position); + _position = state.position; + + while (ch !== 0 && !is_WS_OR_EOL(ch) && !is_FLOW_INDICATOR(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (state.position === _position) { + throwError(state, 'name of an alias node must contain at least one character'); + } + + alias = state.input.slice(_position, state.position); + + if (!_hasOwnProperty$1.call(state.anchorMap, alias)) { + throwError(state, 'unidentified alias "' + alias + '"'); + } + + state.result = state.anchorMap[alias]; + skipSeparationSpace(state, true, -1); + return true; +} + +function composeNode(state, parentIndent, nodeContext, allowToSeek, allowCompact) { + var allowBlockStyles, + allowBlockScalars, + allowBlockCollections, + indentStatus = 1, // 1: this>parent, 0: this=parent, -1: this parentIndent) { + indentStatus = 1; + } else if (state.lineIndent === parentIndent) { + indentStatus = 0; + } else if (state.lineIndent < parentIndent) { + indentStatus = -1; + } + } + } + + if (indentStatus === 1) { + while (readTagProperty(state) || readAnchorProperty(state)) { + if (skipSeparationSpace(state, true, -1)) { + atNewLine = true; + allowBlockCollections = allowBlockStyles; + + if (state.lineIndent > parentIndent) { + indentStatus = 1; + } else if (state.lineIndent === parentIndent) { + indentStatus = 0; + } else if (state.lineIndent < parentIndent) { + indentStatus = -1; + } + } else { + allowBlockCollections = false; + } + } + } + + if (allowBlockCollections) { + allowBlockCollections = atNewLine || allowCompact; + } + + if (indentStatus === 1 || CONTEXT_BLOCK_OUT === nodeContext) { + if (CONTEXT_FLOW_IN === nodeContext || CONTEXT_FLOW_OUT === nodeContext) { + flowIndent = parentIndent; + } else { + flowIndent = parentIndent + 1; + } + + blockIndent = state.position - state.lineStart; + + if (indentStatus === 1) { + if (allowBlockCollections && + (readBlockSequence(state, blockIndent) || + readBlockMapping(state, blockIndent, flowIndent)) || + readFlowCollection(state, flowIndent)) { + hasContent = true; + } else { + if ((allowBlockScalars && readBlockScalar(state, flowIndent)) || + readSingleQuotedScalar(state, flowIndent) || + readDoubleQuotedScalar(state, flowIndent)) { + hasContent = true; + + } else if (readAlias(state)) { + hasContent = true; + + if (state.tag !== null || state.anchor !== null) { + throwError(state, 'alias node should not have any properties'); + } + + } else if (readPlainScalar(state, flowIndent, CONTEXT_FLOW_IN === nodeContext)) { + hasContent = true; + + if (state.tag === null) { + state.tag = '?'; + } + } + + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + } + } else if (indentStatus === 0) { + // Special case: block sequences are allowed to have same indentation level as the parent. + // http://www.yaml.org/spec/1.2/spec.html#id2799784 + hasContent = allowBlockCollections && readBlockSequence(state, blockIndent); + } + } + + if (state.tag === null) { + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + + } else if (state.tag === '?') { + // Implicit resolving is not allowed for non-scalar types, and '?' + // non-specific tag is only automatically assigned to plain scalars. + // + // We only need to check kind conformity in case user explicitly assigns '?' + // tag, for example like this: "! [0]" + // + if (state.result !== null && state.kind !== 'scalar') { + throwError(state, 'unacceptable node kind for ! tag; it should be "scalar", not "' + state.kind + '"'); + } + + for (typeIndex = 0, typeQuantity = state.implicitTypes.length; typeIndex < typeQuantity; typeIndex += 1) { + type = state.implicitTypes[typeIndex]; + + if (type.resolve(state.result)) { // `state.result` updated in resolver if matched + state.result = type.construct(state.result); + state.tag = type.tag; + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + break; + } + } + } else if (state.tag !== '!') { + if (_hasOwnProperty$1.call(state.typeMap[state.kind || 'fallback'], state.tag)) { + type = state.typeMap[state.kind || 'fallback'][state.tag]; + } else { + // looking for multi type + type = null; + typeList = state.typeMap.multi[state.kind || 'fallback']; + + for (typeIndex = 0, typeQuantity = typeList.length; typeIndex < typeQuantity; typeIndex += 1) { + if (state.tag.slice(0, typeList[typeIndex].tag.length) === typeList[typeIndex].tag) { + type = typeList[typeIndex]; + break; + } + } + } + + if (!type) { + throwError(state, 'unknown tag !<' + state.tag + '>'); + } + + if (state.result !== null && type.kind !== state.kind) { + throwError(state, 'unacceptable node kind for !<' + state.tag + '> tag; it should be "' + type.kind + '", not "' + state.kind + '"'); + } + + if (!type.resolve(state.result, state.tag)) { // `state.result` updated in resolver if matched + throwError(state, 'cannot resolve a node with !<' + state.tag + '> explicit tag'); + } else { + state.result = type.construct(state.result, state.tag); + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + } + } + + if (state.listener !== null) { + state.listener('close', state); + } + return state.tag !== null || state.anchor !== null || hasContent; +} + +function readDocument(state) { + var documentStart = state.position, + _position, + directiveName, + directiveArgs, + hasDirectives = false, + ch; + + state.version = null; + state.checkLineBreaks = state.legacy; + state.tagMap = Object.create(null); + state.anchorMap = Object.create(null); + + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + skipSeparationSpace(state, true, -1); + + ch = state.input.charCodeAt(state.position); + + if (state.lineIndent > 0 || ch !== 0x25/* % */) { + break; + } + + hasDirectives = true; + ch = state.input.charCodeAt(++state.position); + _position = state.position; + + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + directiveName = state.input.slice(_position, state.position); + directiveArgs = []; + + if (directiveName.length < 1) { + throwError(state, 'directive name must not be less than one character in length'); + } + + while (ch !== 0) { + while (is_WHITE_SPACE(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (ch === 0x23/* # */) { + do { ch = state.input.charCodeAt(++state.position); } + while (ch !== 0 && !is_EOL(ch)); + break; + } + + if (is_EOL(ch)) break; + + _position = state.position; + + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + directiveArgs.push(state.input.slice(_position, state.position)); + } + + if (ch !== 0) readLineBreak(state); + + if (_hasOwnProperty$1.call(directiveHandlers, directiveName)) { + directiveHandlers[directiveName](state, directiveName, directiveArgs); + } else { + throwWarning(state, 'unknown document directive "' + directiveName + '"'); + } + } + + skipSeparationSpace(state, true, -1); + + if (state.lineIndent === 0 && + state.input.charCodeAt(state.position) === 0x2D/* - */ && + state.input.charCodeAt(state.position + 1) === 0x2D/* - */ && + state.input.charCodeAt(state.position + 2) === 0x2D/* - */) { + state.position += 3; + skipSeparationSpace(state, true, -1); + + } else if (hasDirectives) { + throwError(state, 'directives end mark is expected'); + } + + composeNode(state, state.lineIndent - 1, CONTEXT_BLOCK_OUT, false, true); + skipSeparationSpace(state, true, -1); + + if (state.checkLineBreaks && + PATTERN_NON_ASCII_LINE_BREAKS.test(state.input.slice(documentStart, state.position))) { + throwWarning(state, 'non-ASCII line breaks are interpreted as content'); + } + + state.documents.push(state.result); + + if (state.position === state.lineStart && testDocumentSeparator(state)) { + + if (state.input.charCodeAt(state.position) === 0x2E/* . */) { + state.position += 3; + skipSeparationSpace(state, true, -1); + } + return; + } + + if (state.position < (state.length - 1)) { + throwError(state, 'end of the stream or a document separator is expected'); + } else { + return; + } +} + + +function loadDocuments(input, options) { + input = String(input); + options = options || {}; + + if (input.length !== 0) { + + // Add tailing `\n` if not exists + if (input.charCodeAt(input.length - 1) !== 0x0A/* LF */ && + input.charCodeAt(input.length - 1) !== 0x0D/* CR */) { + input += '\n'; + } + + // Strip BOM + if (input.charCodeAt(0) === 0xFEFF) { + input = input.slice(1); + } + } + + var state = new State$1(input, options); + + var nullpos = input.indexOf('\0'); + + if (nullpos !== -1) { + state.position = nullpos; + throwError(state, 'null byte is not allowed in input'); + } + + // Use 0 as string terminator. That significantly simplifies bounds check. + state.input += '\0'; + + while (state.input.charCodeAt(state.position) === 0x20/* Space */) { + state.lineIndent += 1; + state.position += 1; + } + + while (state.position < (state.length - 1)) { + readDocument(state); + } + + return state.documents; +} + + +function loadAll$1(input, iterator, options) { + if (iterator !== null && typeof iterator === 'object' && typeof options === 'undefined') { + options = iterator; + iterator = null; + } + + var documents = loadDocuments(input, options); + + if (typeof iterator !== 'function') { + return documents; + } + + for (var index = 0, length = documents.length; index < length; index += 1) { + iterator(documents[index]); + } +} + + +function load$1(input, options) { + var documents = loadDocuments(input, options); + + if (documents.length === 0) { + /*eslint-disable no-undefined*/ + return undefined; + } else if (documents.length === 1) { + return documents[0]; + } + throw new exception('expected a single document in the stream, but found more'); +} + + +var loadAll_1 = loadAll$1; +var load_1 = load$1; + +var loader = { + loadAll: loadAll_1, + load: load_1 +}; + +/*eslint-disable no-use-before-define*/ + + + + + +var _toString = Object.prototype.toString; +var _hasOwnProperty = Object.prototype.hasOwnProperty; + +var CHAR_BOM = 0xFEFF; +var CHAR_TAB = 0x09; /* Tab */ +var CHAR_LINE_FEED = 0x0A; /* LF */ +var CHAR_CARRIAGE_RETURN = 0x0D; /* CR */ +var CHAR_SPACE = 0x20; /* Space */ +var CHAR_EXCLAMATION = 0x21; /* ! */ +var CHAR_DOUBLE_QUOTE = 0x22; /* " */ +var CHAR_SHARP = 0x23; /* # */ +var CHAR_PERCENT = 0x25; /* % */ +var CHAR_AMPERSAND = 0x26; /* & */ +var CHAR_SINGLE_QUOTE = 0x27; /* ' */ +var CHAR_ASTERISK = 0x2A; /* * */ +var CHAR_COMMA = 0x2C; /* , */ +var CHAR_MINUS = 0x2D; /* - */ +var CHAR_COLON = 0x3A; /* : */ +var CHAR_EQUALS = 0x3D; /* = */ +var CHAR_GREATER_THAN = 0x3E; /* > */ +var CHAR_QUESTION = 0x3F; /* ? */ +var CHAR_COMMERCIAL_AT = 0x40; /* @ */ +var CHAR_LEFT_SQUARE_BRACKET = 0x5B; /* [ */ +var CHAR_RIGHT_SQUARE_BRACKET = 0x5D; /* ] */ +var CHAR_GRAVE_ACCENT = 0x60; /* ` */ +var CHAR_LEFT_CURLY_BRACKET = 0x7B; /* { */ +var CHAR_VERTICAL_LINE = 0x7C; /* | */ +var CHAR_RIGHT_CURLY_BRACKET = 0x7D; /* } */ + +var ESCAPE_SEQUENCES = {}; + +ESCAPE_SEQUENCES[0x00] = '\\0'; +ESCAPE_SEQUENCES[0x07] = '\\a'; +ESCAPE_SEQUENCES[0x08] = '\\b'; +ESCAPE_SEQUENCES[0x09] = '\\t'; +ESCAPE_SEQUENCES[0x0A] = '\\n'; +ESCAPE_SEQUENCES[0x0B] = '\\v'; +ESCAPE_SEQUENCES[0x0C] = '\\f'; +ESCAPE_SEQUENCES[0x0D] = '\\r'; +ESCAPE_SEQUENCES[0x1B] = '\\e'; +ESCAPE_SEQUENCES[0x22] = '\\"'; +ESCAPE_SEQUENCES[0x5C] = '\\\\'; +ESCAPE_SEQUENCES[0x85] = '\\N'; +ESCAPE_SEQUENCES[0xA0] = '\\_'; +ESCAPE_SEQUENCES[0x2028] = '\\L'; +ESCAPE_SEQUENCES[0x2029] = '\\P'; + +var DEPRECATED_BOOLEANS_SYNTAX = [ + 'y', 'Y', 'yes', 'Yes', 'YES', 'on', 'On', 'ON', + 'n', 'N', 'no', 'No', 'NO', 'off', 'Off', 'OFF' +]; + +var DEPRECATED_BASE60_SYNTAX = /^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/; + +function compileStyleMap(schema, map) { + var result, keys, index, length, tag, style, type; + + if (map === null) return {}; + + result = {}; + keys = Object.keys(map); + + for (index = 0, length = keys.length; index < length; index += 1) { + tag = keys[index]; + style = String(map[tag]); + + if (tag.slice(0, 2) === '!!') { + tag = 'tag:yaml.org,2002:' + tag.slice(2); + } + type = schema.compiledTypeMap['fallback'][tag]; + + if (type && _hasOwnProperty.call(type.styleAliases, style)) { + style = type.styleAliases[style]; + } + + result[tag] = style; + } + + return result; +} + +function encodeHex(character) { + var string, handle, length; + + string = character.toString(16).toUpperCase(); + + if (character <= 0xFF) { + handle = 'x'; + length = 2; + } else if (character <= 0xFFFF) { + handle = 'u'; + length = 4; + } else if (character <= 0xFFFFFFFF) { + handle = 'U'; + length = 8; + } else { + throw new exception('code point within a string may not be greater than 0xFFFFFFFF'); + } + + return '\\' + handle + common.repeat('0', length - string.length) + string; +} + + +var QUOTING_TYPE_SINGLE = 1, + QUOTING_TYPE_DOUBLE = 2; + +function State(options) { + this.schema = options['schema'] || _default; + this.indent = Math.max(1, (options['indent'] || 2)); + this.noArrayIndent = options['noArrayIndent'] || false; + this.skipInvalid = options['skipInvalid'] || false; + this.flowLevel = (common.isNothing(options['flowLevel']) ? -1 : options['flowLevel']); + this.styleMap = compileStyleMap(this.schema, options['styles'] || null); + this.sortKeys = options['sortKeys'] || false; + this.lineWidth = options['lineWidth'] || 80; + this.noRefs = options['noRefs'] || false; + this.noCompatMode = options['noCompatMode'] || false; + this.condenseFlow = options['condenseFlow'] || false; + this.quotingType = options['quotingType'] === '"' ? QUOTING_TYPE_DOUBLE : QUOTING_TYPE_SINGLE; + this.forceQuotes = options['forceQuotes'] || false; + this.replacer = typeof options['replacer'] === 'function' ? options['replacer'] : null; + + this.implicitTypes = this.schema.compiledImplicit; + this.explicitTypes = this.schema.compiledExplicit; + + this.tag = null; + this.result = ''; + + this.duplicates = []; + this.usedDuplicates = null; +} + +// Indents every line in a string. Empty lines (\n only) are not indented. +function indentString(string, spaces) { + var ind = common.repeat(' ', spaces), + position = 0, + next = -1, + result = '', + line, + length = string.length; + + while (position < length) { + next = string.indexOf('\n', position); + if (next === -1) { + line = string.slice(position); + position = length; + } else { + line = string.slice(position, next + 1); + position = next + 1; + } + + if (line.length && line !== '\n') result += ind; + + result += line; + } + + return result; +} + +function generateNextLine(state, level) { + return '\n' + common.repeat(' ', state.indent * level); +} + +function testImplicitResolving(state, str) { + var index, length, type; + + for (index = 0, length = state.implicitTypes.length; index < length; index += 1) { + type = state.implicitTypes[index]; + + if (type.resolve(str)) { + return true; + } + } + + return false; +} + +// [33] s-white ::= s-space | s-tab +function isWhitespace(c) { + return c === CHAR_SPACE || c === CHAR_TAB; +} + +// Returns true if the character can be printed without escaping. +// From YAML 1.2: "any allowed characters known to be non-printable +// should also be escaped. [However,] This isn’t mandatory" +// Derived from nb-char - \t - #x85 - #xA0 - #x2028 - #x2029. +function isPrintable(c) { + return (0x00020 <= c && c <= 0x00007E) + || ((0x000A1 <= c && c <= 0x00D7FF) && c !== 0x2028 && c !== 0x2029) + || ((0x0E000 <= c && c <= 0x00FFFD) && c !== CHAR_BOM) + || (0x10000 <= c && c <= 0x10FFFF); +} + +// [34] ns-char ::= nb-char - s-white +// [27] nb-char ::= c-printable - b-char - c-byte-order-mark +// [26] b-char ::= b-line-feed | b-carriage-return +// Including s-white (for some reason, examples doesn't match specs in this aspect) +// ns-char ::= c-printable - b-line-feed - b-carriage-return - c-byte-order-mark +function isNsCharOrWhitespace(c) { + return isPrintable(c) + && c !== CHAR_BOM + // - b-char + && c !== CHAR_CARRIAGE_RETURN + && c !== CHAR_LINE_FEED; +} + +// [127] ns-plain-safe(c) ::= c = flow-out ⇒ ns-plain-safe-out +// c = flow-in ⇒ ns-plain-safe-in +// c = block-key ⇒ ns-plain-safe-out +// c = flow-key ⇒ ns-plain-safe-in +// [128] ns-plain-safe-out ::= ns-char +// [129] ns-plain-safe-in ::= ns-char - c-flow-indicator +// [130] ns-plain-char(c) ::= ( ns-plain-safe(c) - “:” - “#” ) +// | ( /* An ns-char preceding */ “#” ) +// | ( “:” /* Followed by an ns-plain-safe(c) */ ) +function isPlainSafe(c, prev, inblock) { + var cIsNsCharOrWhitespace = isNsCharOrWhitespace(c); + var cIsNsChar = cIsNsCharOrWhitespace && !isWhitespace(c); + return ( + // ns-plain-safe + inblock ? // c = flow-in + cIsNsCharOrWhitespace + : cIsNsCharOrWhitespace + // - c-flow-indicator + && c !== CHAR_COMMA + && c !== CHAR_LEFT_SQUARE_BRACKET + && c !== CHAR_RIGHT_SQUARE_BRACKET + && c !== CHAR_LEFT_CURLY_BRACKET + && c !== CHAR_RIGHT_CURLY_BRACKET + ) + // ns-plain-char + && c !== CHAR_SHARP // false on '#' + && !(prev === CHAR_COLON && !cIsNsChar) // false on ': ' + || (isNsCharOrWhitespace(prev) && !isWhitespace(prev) && c === CHAR_SHARP) // change to true on '[^ ]#' + || (prev === CHAR_COLON && cIsNsChar); // change to true on ':[^ ]' +} + +// Simplified test for values allowed as the first character in plain style. +function isPlainSafeFirst(c) { + // Uses a subset of ns-char - c-indicator + // where ns-char = nb-char - s-white. + // No support of ( ( “?” | “:” | “-” ) /* Followed by an ns-plain-safe(c)) */ ) part + return isPrintable(c) && c !== CHAR_BOM + && !isWhitespace(c) // - s-white + // - (c-indicator ::= + // “-” | “?” | “:” | “,” | “[” | “]” | “{” | “}” + && c !== CHAR_MINUS + && c !== CHAR_QUESTION + && c !== CHAR_COLON + && c !== CHAR_COMMA + && c !== CHAR_LEFT_SQUARE_BRACKET + && c !== CHAR_RIGHT_SQUARE_BRACKET + && c !== CHAR_LEFT_CURLY_BRACKET + && c !== CHAR_RIGHT_CURLY_BRACKET + // | “#” | “&” | “*” | “!” | “|” | “=” | “>” | “'” | “"” + && c !== CHAR_SHARP + && c !== CHAR_AMPERSAND + && c !== CHAR_ASTERISK + && c !== CHAR_EXCLAMATION + && c !== CHAR_VERTICAL_LINE + && c !== CHAR_EQUALS + && c !== CHAR_GREATER_THAN + && c !== CHAR_SINGLE_QUOTE + && c !== CHAR_DOUBLE_QUOTE + // | “%” | “@” | “`”) + && c !== CHAR_PERCENT + && c !== CHAR_COMMERCIAL_AT + && c !== CHAR_GRAVE_ACCENT; +} + +// Simplified test for values allowed as the last character in plain style. +function isPlainSafeLast(c) { + // just not whitespace or colon, it will be checked to be plain character later + return !isWhitespace(c) && c !== CHAR_COLON; +} + +// Same as 'string'.codePointAt(pos), but works in older browsers. +function codePointAt(string, pos) { + var first = string.charCodeAt(pos), second; + if (first >= 0xD800 && first <= 0xDBFF && pos + 1 < string.length) { + second = string.charCodeAt(pos + 1); + if (second >= 0xDC00 && second <= 0xDFFF) { + // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae + return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } + } + return first; +} + +// Determines whether block indentation indicator is required. +function needIndentIndicator(string) { + var leadingSpaceRe = /^\n* /; + return leadingSpaceRe.test(string); +} + +var STYLE_PLAIN = 1, + STYLE_SINGLE = 2, + STYLE_LITERAL = 3, + STYLE_FOLDED = 4, + STYLE_DOUBLE = 5; + +// Determines which scalar styles are possible and returns the preferred style. +// lineWidth = -1 => no limit. +// Pre-conditions: str.length > 0. +// Post-conditions: +// STYLE_PLAIN or STYLE_SINGLE => no \n are in the string. +// STYLE_LITERAL => no lines are suitable for folding (or lineWidth is -1). +// STYLE_FOLDED => a line > lineWidth and can be folded (and lineWidth != -1). +function chooseScalarStyle(string, singleLineOnly, indentPerLevel, lineWidth, + testAmbiguousType, quotingType, forceQuotes, inblock) { + + var i; + var char = 0; + var prevChar = null; + var hasLineBreak = false; + var hasFoldableLine = false; // only checked if shouldTrackWidth + var shouldTrackWidth = lineWidth !== -1; + var previousLineBreak = -1; // count the first line correctly + var plain = isPlainSafeFirst(codePointAt(string, 0)) + && isPlainSafeLast(codePointAt(string, string.length - 1)); + + if (singleLineOnly || forceQuotes) { + // Case: no block styles. + // Check for disallowed characters to rule out plain and single. + for (i = 0; i < string.length; char >= 0x10000 ? i += 2 : i++) { + char = codePointAt(string, i); + if (!isPrintable(char)) { + return STYLE_DOUBLE; + } + plain = plain && isPlainSafe(char, prevChar, inblock); + prevChar = char; + } + } else { + // Case: block styles permitted. + for (i = 0; i < string.length; char >= 0x10000 ? i += 2 : i++) { + char = codePointAt(string, i); + if (char === CHAR_LINE_FEED) { + hasLineBreak = true; + // Check if any line can be folded. + if (shouldTrackWidth) { + hasFoldableLine = hasFoldableLine || + // Foldable line = too long, and not more-indented. + (i - previousLineBreak - 1 > lineWidth && + string[previousLineBreak + 1] !== ' '); + previousLineBreak = i; + } + } else if (!isPrintable(char)) { + return STYLE_DOUBLE; + } + plain = plain && isPlainSafe(char, prevChar, inblock); + prevChar = char; + } + // in case the end is missing a \n + hasFoldableLine = hasFoldableLine || (shouldTrackWidth && + (i - previousLineBreak - 1 > lineWidth && + string[previousLineBreak + 1] !== ' ')); + } + // Although every style can represent \n without escaping, prefer block styles + // for multiline, since they're more readable and they don't add empty lines. + // Also prefer folding a super-long line. + if (!hasLineBreak && !hasFoldableLine) { + // Strings interpretable as another type have to be quoted; + // e.g. the string 'true' vs. the boolean true. + if (plain && !forceQuotes && !testAmbiguousType(string)) { + return STYLE_PLAIN; + } + return quotingType === QUOTING_TYPE_DOUBLE ? STYLE_DOUBLE : STYLE_SINGLE; + } + // Edge case: block indentation indicator can only have one digit. + if (indentPerLevel > 9 && needIndentIndicator(string)) { + return STYLE_DOUBLE; + } + // At this point we know block styles are valid. + // Prefer literal style unless we want to fold. + if (!forceQuotes) { + return hasFoldableLine ? STYLE_FOLDED : STYLE_LITERAL; + } + return quotingType === QUOTING_TYPE_DOUBLE ? STYLE_DOUBLE : STYLE_SINGLE; +} + +// Note: line breaking/folding is implemented for only the folded style. +// NB. We drop the last trailing newline (if any) of a returned block scalar +// since the dumper adds its own newline. This always works: +// • No ending newline => unaffected; already using strip "-" chomping. +// • Ending newline => removed then restored. +// Importantly, this keeps the "+" chomp indicator from gaining an extra line. +function writeScalar(state, string, level, iskey, inblock) { + state.dump = (function () { + if (string.length === 0) { + return state.quotingType === QUOTING_TYPE_DOUBLE ? '""' : "''"; + } + if (!state.noCompatMode) { + if (DEPRECATED_BOOLEANS_SYNTAX.indexOf(string) !== -1 || DEPRECATED_BASE60_SYNTAX.test(string)) { + return state.quotingType === QUOTING_TYPE_DOUBLE ? ('"' + string + '"') : ("'" + string + "'"); + } + } + + var indent = state.indent * Math.max(1, level); // no 0-indent scalars + // As indentation gets deeper, let the width decrease monotonically + // to the lower bound min(state.lineWidth, 40). + // Note that this implies + // state.lineWidth ≤ 40 + state.indent: width is fixed at the lower bound. + // state.lineWidth > 40 + state.indent: width decreases until the lower bound. + // This behaves better than a constant minimum width which disallows narrower options, + // or an indent threshold which causes the width to suddenly increase. + var lineWidth = state.lineWidth === -1 + ? -1 : Math.max(Math.min(state.lineWidth, 40), state.lineWidth - indent); + + // Without knowing if keys are implicit/explicit, assume implicit for safety. + var singleLineOnly = iskey + // No block styles in flow mode. + || (state.flowLevel > -1 && level >= state.flowLevel); + function testAmbiguity(string) { + return testImplicitResolving(state, string); + } + + switch (chooseScalarStyle(string, singleLineOnly, state.indent, lineWidth, + testAmbiguity, state.quotingType, state.forceQuotes && !iskey, inblock)) { + + case STYLE_PLAIN: + return string; + case STYLE_SINGLE: + return "'" + string.replace(/'/g, "''") + "'"; + case STYLE_LITERAL: + return '|' + blockHeader(string, state.indent) + + dropEndingNewline(indentString(string, indent)); + case STYLE_FOLDED: + return '>' + blockHeader(string, state.indent) + + dropEndingNewline(indentString(foldString(string, lineWidth), indent)); + case STYLE_DOUBLE: + return '"' + escapeString(string) + '"'; + default: + throw new exception('impossible error: invalid scalar style'); + } + }()); +} + +// Pre-conditions: string is valid for a block scalar, 1 <= indentPerLevel <= 9. +function blockHeader(string, indentPerLevel) { + var indentIndicator = needIndentIndicator(string) ? String(indentPerLevel) : ''; + + // note the special case: the string '\n' counts as a "trailing" empty line. + var clip = string[string.length - 1] === '\n'; + var keep = clip && (string[string.length - 2] === '\n' || string === '\n'); + var chomp = keep ? '+' : (clip ? '' : '-'); + + return indentIndicator + chomp + '\n'; +} + +// (See the note for writeScalar.) +function dropEndingNewline(string) { + return string[string.length - 1] === '\n' ? string.slice(0, -1) : string; +} + +// Note: a long line without a suitable break point will exceed the width limit. +// Pre-conditions: every char in str isPrintable, str.length > 0, width > 0. +function foldString(string, width) { + // In folded style, $k$ consecutive newlines output as $k+1$ newlines— + // unless they're before or after a more-indented line, or at the very + // beginning or end, in which case $k$ maps to $k$. + // Therefore, parse each chunk as newline(s) followed by a content line. + var lineRe = /(\n+)([^\n]*)/g; + + // first line (possibly an empty line) + var result = (function () { + var nextLF = string.indexOf('\n'); + nextLF = nextLF !== -1 ? nextLF : string.length; + lineRe.lastIndex = nextLF; + return foldLine(string.slice(0, nextLF), width); + }()); + // If we haven't reached the first content line yet, don't add an extra \n. + var prevMoreIndented = string[0] === '\n' || string[0] === ' '; + var moreIndented; + + // rest of the lines + var match; + while ((match = lineRe.exec(string))) { + var prefix = match[1], line = match[2]; + moreIndented = (line[0] === ' '); + result += prefix + + (!prevMoreIndented && !moreIndented && line !== '' + ? '\n' : '') + + foldLine(line, width); + prevMoreIndented = moreIndented; + } + + return result; +} + +// Greedy line breaking. +// Picks the longest line under the limit each time, +// otherwise settles for the shortest line over the limit. +// NB. More-indented lines *cannot* be folded, as that would add an extra \n. +function foldLine(line, width) { + if (line === '' || line[0] === ' ') return line; + + // Since a more-indented line adds a \n, breaks can't be followed by a space. + var breakRe = / [^ ]/g; // note: the match index will always be <= length-2. + var match; + // start is an inclusive index. end, curr, and next are exclusive. + var start = 0, end, curr = 0, next = 0; + var result = ''; + + // Invariants: 0 <= start <= length-1. + // 0 <= curr <= next <= max(0, length-2). curr - start <= width. + // Inside the loop: + // A match implies length >= 2, so curr and next are <= length-2. + while ((match = breakRe.exec(line))) { + next = match.index; + // maintain invariant: curr - start <= width + if (next - start > width) { + end = (curr > start) ? curr : next; // derive end <= length-2 + result += '\n' + line.slice(start, end); + // skip the space that was output as \n + start = end + 1; // derive start <= length-1 + } + curr = next; + } + + // By the invariants, start <= length-1, so there is something left over. + // It is either the whole string or a part starting from non-whitespace. + result += '\n'; + // Insert a break if the remainder is too long and there is a break available. + if (line.length - start > width && curr > start) { + result += line.slice(start, curr) + '\n' + line.slice(curr + 1); + } else { + result += line.slice(start); + } + + return result.slice(1); // drop extra \n joiner +} + +// Escapes a double-quoted string. +function escapeString(string) { + var result = ''; + var char = 0; + var escapeSeq; + + for (var i = 0; i < string.length; char >= 0x10000 ? i += 2 : i++) { + char = codePointAt(string, i); + escapeSeq = ESCAPE_SEQUENCES[char]; + + if (!escapeSeq && isPrintable(char)) { + result += string[i]; + if (char >= 0x10000) result += string[i + 1]; + } else { + result += escapeSeq || encodeHex(char); + } + } + + return result; +} + +function writeFlowSequence(state, level, object) { + var _result = '', + _tag = state.tag, + index, + length, + value; + + for (index = 0, length = object.length; index < length; index += 1) { + value = object[index]; + + if (state.replacer) { + value = state.replacer.call(object, String(index), value); + } + + // Write only valid elements, put null instead of invalid elements. + if (writeNode(state, level, value, false, false) || + (typeof value === 'undefined' && + writeNode(state, level, null, false, false))) { + + if (_result !== '') _result += ',' + (!state.condenseFlow ? ' ' : ''); + _result += state.dump; + } + } + + state.tag = _tag; + state.dump = '[' + _result + ']'; +} + +function writeBlockSequence(state, level, object, compact) { + var _result = '', + _tag = state.tag, + index, + length, + value; + + for (index = 0, length = object.length; index < length; index += 1) { + value = object[index]; + + if (state.replacer) { + value = state.replacer.call(object, String(index), value); + } + + // Write only valid elements, put null instead of invalid elements. + if (writeNode(state, level + 1, value, true, true, false, true) || + (typeof value === 'undefined' && + writeNode(state, level + 1, null, true, true, false, true))) { + + if (!compact || _result !== '') { + _result += generateNextLine(state, level); + } + + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + _result += '-'; + } else { + _result += '- '; + } + + _result += state.dump; + } + } + + state.tag = _tag; + state.dump = _result || '[]'; // Empty sequence if no valid values. +} + +function writeFlowMapping(state, level, object) { + var _result = '', + _tag = state.tag, + objectKeyList = Object.keys(object), + index, + length, + objectKey, + objectValue, + pairBuffer; + + for (index = 0, length = objectKeyList.length; index < length; index += 1) { + + pairBuffer = ''; + if (_result !== '') pairBuffer += ', '; + + if (state.condenseFlow) pairBuffer += '"'; + + objectKey = objectKeyList[index]; + objectValue = object[objectKey]; + + if (state.replacer) { + objectValue = state.replacer.call(object, objectKey, objectValue); + } + + if (!writeNode(state, level, objectKey, false, false)) { + continue; // Skip this pair because of invalid key; + } + + if (state.dump.length > 1024) pairBuffer += '? '; + + pairBuffer += state.dump + (state.condenseFlow ? '"' : '') + ':' + (state.condenseFlow ? '' : ' '); + + if (!writeNode(state, level, objectValue, false, false)) { + continue; // Skip this pair because of invalid value. + } + + pairBuffer += state.dump; + + // Both key and value are valid. + _result += pairBuffer; + } + + state.tag = _tag; + state.dump = '{' + _result + '}'; +} + +function writeBlockMapping(state, level, object, compact) { + var _result = '', + _tag = state.tag, + objectKeyList = Object.keys(object), + index, + length, + objectKey, + objectValue, + explicitPair, + pairBuffer; + + // Allow sorting keys so that the output file is deterministic + if (state.sortKeys === true) { + // Default sorting + objectKeyList.sort(); + } else if (typeof state.sortKeys === 'function') { + // Custom sort function + objectKeyList.sort(state.sortKeys); + } else if (state.sortKeys) { + // Something is wrong + throw new exception('sortKeys must be a boolean or a function'); + } + + for (index = 0, length = objectKeyList.length; index < length; index += 1) { + pairBuffer = ''; + + if (!compact || _result !== '') { + pairBuffer += generateNextLine(state, level); + } + + objectKey = objectKeyList[index]; + objectValue = object[objectKey]; + + if (state.replacer) { + objectValue = state.replacer.call(object, objectKey, objectValue); + } + + if (!writeNode(state, level + 1, objectKey, true, true, true)) { + continue; // Skip this pair because of invalid key. + } + + explicitPair = (state.tag !== null && state.tag !== '?') || + (state.dump && state.dump.length > 1024); + + if (explicitPair) { + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + pairBuffer += '?'; + } else { + pairBuffer += '? '; + } + } + + pairBuffer += state.dump; + + if (explicitPair) { + pairBuffer += generateNextLine(state, level); + } + + if (!writeNode(state, level + 1, objectValue, true, explicitPair)) { + continue; // Skip this pair because of invalid value. + } + + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + pairBuffer += ':'; + } else { + pairBuffer += ': '; + } + + pairBuffer += state.dump; + + // Both key and value are valid. + _result += pairBuffer; + } + + state.tag = _tag; + state.dump = _result || '{}'; // Empty mapping if no valid pairs. +} + +function detectType(state, object, explicit) { + var _result, typeList, index, length, type, style; + + typeList = explicit ? state.explicitTypes : state.implicitTypes; + + for (index = 0, length = typeList.length; index < length; index += 1) { + type = typeList[index]; + + if ((type.instanceOf || type.predicate) && + (!type.instanceOf || ((typeof object === 'object') && (object instanceof type.instanceOf))) && + (!type.predicate || type.predicate(object))) { + + if (explicit) { + if (type.multi && type.representName) { + state.tag = type.representName(object); + } else { + state.tag = type.tag; + } + } else { + state.tag = '?'; + } + + if (type.represent) { + style = state.styleMap[type.tag] || type.defaultStyle; + + if (_toString.call(type.represent) === '[object Function]') { + _result = type.represent(object, style); + } else if (_hasOwnProperty.call(type.represent, style)) { + _result = type.represent[style](object, style); + } else { + throw new exception('!<' + type.tag + '> tag resolver accepts not "' + style + '" style'); + } + + state.dump = _result; + } + + return true; + } + } + + return false; +} + +// Serializes `object` and writes it to global `result`. +// Returns true on success, or false on invalid object. +// +function writeNode(state, level, object, block, compact, iskey, isblockseq) { + state.tag = null; + state.dump = object; + + if (!detectType(state, object, false)) { + detectType(state, object, true); + } + + var type = _toString.call(state.dump); + var inblock = block; + var tagStr; + + if (block) { + block = (state.flowLevel < 0 || state.flowLevel > level); + } + + var objectOrArray = type === '[object Object]' || type === '[object Array]', + duplicateIndex, + duplicate; + + if (objectOrArray) { + duplicateIndex = state.duplicates.indexOf(object); + duplicate = duplicateIndex !== -1; + } + + if ((state.tag !== null && state.tag !== '?') || duplicate || (state.indent !== 2 && level > 0)) { + compact = false; + } + + if (duplicate && state.usedDuplicates[duplicateIndex]) { + state.dump = '*ref_' + duplicateIndex; + } else { + if (objectOrArray && duplicate && !state.usedDuplicates[duplicateIndex]) { + state.usedDuplicates[duplicateIndex] = true; + } + if (type === '[object Object]') { + if (block && (Object.keys(state.dump).length !== 0)) { + writeBlockMapping(state, level, state.dump, compact); + if (duplicate) { + state.dump = '&ref_' + duplicateIndex + state.dump; + } + } else { + writeFlowMapping(state, level, state.dump); + if (duplicate) { + state.dump = '&ref_' + duplicateIndex + ' ' + state.dump; + } + } + } else if (type === '[object Array]') { + if (block && (state.dump.length !== 0)) { + if (state.noArrayIndent && !isblockseq && level > 0) { + writeBlockSequence(state, level - 1, state.dump, compact); + } else { + writeBlockSequence(state, level, state.dump, compact); + } + if (duplicate) { + state.dump = '&ref_' + duplicateIndex + state.dump; + } + } else { + writeFlowSequence(state, level, state.dump); + if (duplicate) { + state.dump = '&ref_' + duplicateIndex + ' ' + state.dump; + } + } + } else if (type === '[object String]') { + if (state.tag !== '?') { + writeScalar(state, state.dump, level, iskey, inblock); + } + } else if (type === '[object Undefined]') { + return false; + } else { + if (state.skipInvalid) return false; + throw new exception('unacceptable kind of an object to dump ' + type); + } + + if (state.tag !== null && state.tag !== '?') { + // Need to encode all characters except those allowed by the spec: + // + // [35] ns-dec-digit ::= [#x30-#x39] /* 0-9 */ + // [36] ns-hex-digit ::= ns-dec-digit + // | [#x41-#x46] /* A-F */ | [#x61-#x66] /* a-f */ + // [37] ns-ascii-letter ::= [#x41-#x5A] /* A-Z */ | [#x61-#x7A] /* a-z */ + // [38] ns-word-char ::= ns-dec-digit | ns-ascii-letter | “-” + // [39] ns-uri-char ::= “%” ns-hex-digit ns-hex-digit | ns-word-char | “#” + // | “;” | “/” | “?” | “:” | “@” | “&” | “=” | “+” | “$” | “,” + // | “_” | “.” | “!” | “~” | “*” | “'” | “(” | “)” | “[” | “]” + // + // Also need to encode '!' because it has special meaning (end of tag prefix). + // + tagStr = encodeURI( + state.tag[0] === '!' ? state.tag.slice(1) : state.tag + ).replace(/!/g, '%21'); + + if (state.tag[0] === '!') { + tagStr = '!' + tagStr; + } else if (tagStr.slice(0, 18) === 'tag:yaml.org,2002:') { + tagStr = '!!' + tagStr.slice(18); + } else { + tagStr = '!<' + tagStr + '>'; + } + + state.dump = tagStr + ' ' + state.dump; + } + } + + return true; +} + +function getDuplicateReferences(object, state) { + var objects = [], + duplicatesIndexes = [], + index, + length; + + inspectNode(object, objects, duplicatesIndexes); + + for (index = 0, length = duplicatesIndexes.length; index < length; index += 1) { + state.duplicates.push(objects[duplicatesIndexes[index]]); + } + state.usedDuplicates = new Array(length); +} + +function inspectNode(object, objects, duplicatesIndexes) { + var objectKeyList, + index, + length; + + if (object !== null && typeof object === 'object') { + index = objects.indexOf(object); + if (index !== -1) { + if (duplicatesIndexes.indexOf(index) === -1) { + duplicatesIndexes.push(index); + } + } else { + objects.push(object); + + if (Array.isArray(object)) { + for (index = 0, length = object.length; index < length; index += 1) { + inspectNode(object[index], objects, duplicatesIndexes); + } + } else { + objectKeyList = Object.keys(object); + + for (index = 0, length = objectKeyList.length; index < length; index += 1) { + inspectNode(object[objectKeyList[index]], objects, duplicatesIndexes); + } + } + } + } +} + +function dump$1(input, options) { + options = options || {}; + + var state = new State(options); + + if (!state.noRefs) getDuplicateReferences(input, state); + + var value = input; + + if (state.replacer) { + value = state.replacer.call({ '': value }, '', value); + } + + if (writeNode(state, 0, value, true, true)) return state.dump + '\n'; + + return ''; +} + +var dump_1 = dump$1; + +var dumper = { + dump: dump_1 +}; + +function renamed(from, to) { + return function () { + throw new Error('Function yaml.' + from + ' is removed in js-yaml 4. ' + + 'Use yaml.' + to + ' instead, which is now safe by default.'); + }; +} + + +var Type = type; +var Schema = schema; +var FAILSAFE_SCHEMA = failsafe; +var JSON_SCHEMA = json; +var CORE_SCHEMA = core; +var DEFAULT_SCHEMA = _default; +var load = loader.load; +var loadAll = loader.loadAll; +var dump = dumper.dump; +var YAMLException = exception; + +// Re-export all types in case user wants to create custom schema +var types = { + binary: binary, + float: float, + map: map, + null: _null, + pairs: pairs, + set: set, + timestamp: timestamp, + bool: bool, + int: int, + merge: merge, + omap: omap, + seq: seq, + str: str +}; + +// Removed functions from JS-YAML 3.0.x +var safeLoad = renamed('safeLoad', 'load'); +var safeLoadAll = renamed('safeLoadAll', 'loadAll'); +var safeDump = renamed('safeDump', 'dump'); + +var jsYaml = { + Type: Type, + Schema: Schema, + FAILSAFE_SCHEMA: FAILSAFE_SCHEMA, + JSON_SCHEMA: JSON_SCHEMA, + CORE_SCHEMA: CORE_SCHEMA, + DEFAULT_SCHEMA: DEFAULT_SCHEMA, + load: load, + loadAll: loadAll, + dump: dump, + YAMLException: YAMLException, + types: types, + safeLoad: safeLoad, + safeLoadAll: safeLoadAll, + safeDump: safeDump +}; + +export default jsYaml; +export { CORE_SCHEMA, DEFAULT_SCHEMA, FAILSAFE_SCHEMA, JSON_SCHEMA, Schema, Type, YAMLException, dump, load, loadAll, safeDump, safeLoad, safeLoadAll, types }; diff --git a/libs/minisearch.mjs b/libs/minisearch.mjs new file mode 100644 index 0000000..a05fe7b --- /dev/null +++ b/libs/minisearch.mjs @@ -0,0 +1,2036 @@ +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise, SuppressedError, Symbol */ + + +function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; +}; + +/** @ignore */ +const ENTRIES = 'ENTRIES'; +/** @ignore */ +const KEYS = 'KEYS'; +/** @ignore */ +const VALUES = 'VALUES'; +/** @ignore */ +const LEAF = ''; +/** + * @private + */ +class TreeIterator { + constructor(set, type) { + const node = set._tree; + const keys = Array.from(node.keys()); + this.set = set; + this._type = type; + this._path = keys.length > 0 ? [{ node, keys }] : []; + } + next() { + const value = this.dive(); + this.backtrack(); + return value; + } + dive() { + if (this._path.length === 0) { + return { done: true, value: undefined }; + } + const { node, keys } = last$1(this._path); + if (last$1(keys) === LEAF) { + return { done: false, value: this.result() }; + } + const child = node.get(last$1(keys)); + this._path.push({ node: child, keys: Array.from(child.keys()) }); + return this.dive(); + } + backtrack() { + if (this._path.length === 0) { + return; + } + const keys = last$1(this._path).keys; + keys.pop(); + if (keys.length > 0) { + return; + } + this._path.pop(); + this.backtrack(); + } + key() { + return this.set._prefix + this._path + .map(({ keys }) => last$1(keys)) + .filter(key => key !== LEAF) + .join(''); + } + value() { + return last$1(this._path).node.get(LEAF); + } + result() { + switch (this._type) { + case VALUES: return this.value(); + case KEYS: return this.key(); + default: return [this.key(), this.value()]; + } + } + [Symbol.iterator]() { + return this; + } +} +const last$1 = (array) => { + return array[array.length - 1]; +}; + +/* eslint-disable no-labels */ +/** + * @ignore + */ +const fuzzySearch = (node, query, maxDistance) => { + const results = new Map(); + if (query === undefined) + return results; + // Number of columns in the Levenshtein matrix. + const n = query.length + 1; + // Matching terms can never be longer than N + maxDistance. + const m = n + maxDistance; + // Fill first matrix row and column with numbers: 0 1 2 3 ... + const matrix = new Uint8Array(m * n).fill(maxDistance + 1); + for (let j = 0; j < n; ++j) + matrix[j] = j; + for (let i = 1; i < m; ++i) + matrix[i * n] = i; + recurse(node, query, maxDistance, results, matrix, 1, n, ''); + return results; +}; +// Modified version of http://stevehanov.ca/blog/?id=114 +// This builds a Levenshtein matrix for a given query and continuously updates +// it for nodes in the radix tree that fall within the given maximum edit +// distance. Keeping the same matrix around is beneficial especially for larger +// edit distances. +// +// k a t e <-- query +// 0 1 2 3 4 +// c 1 1 2 3 4 +// a 2 2 1 2 3 +// t 3 3 2 1 [2] <-- edit distance +// ^ +// ^ term in radix tree, rows are added and removed as needed +const recurse = (node, query, maxDistance, results, matrix, m, n, prefix) => { + const offset = m * n; + key: for (const key of node.keys()) { + if (key === LEAF) { + // We've reached a leaf node. Check if the edit distance acceptable and + // store the result if it is. + const distance = matrix[offset - 1]; + if (distance <= maxDistance) { + results.set(prefix, [node.get(key), distance]); + } + } + else { + // Iterate over all characters in the key. Update the Levenshtein matrix + // and check if the minimum distance in the last row is still within the + // maximum edit distance. If it is, we can recurse over all child nodes. + let i = m; + for (let pos = 0; pos < key.length; ++pos, ++i) { + const char = key[pos]; + const thisRowOffset = n * i; + const prevRowOffset = thisRowOffset - n; + // Set the first column based on the previous row, and initialize the + // minimum distance in the current row. + let minDistance = matrix[thisRowOffset]; + const jmin = Math.max(0, i - maxDistance - 1); + const jmax = Math.min(n - 1, i + maxDistance); + // Iterate over remaining columns (characters in the query). + for (let j = jmin; j < jmax; ++j) { + const different = char !== query[j]; + // It might make sense to only read the matrix positions used for + // deletion/insertion if the characters are different. But we want to + // avoid conditional reads for performance reasons. + const rpl = matrix[prevRowOffset + j] + +different; + const del = matrix[prevRowOffset + j + 1] + 1; + const ins = matrix[thisRowOffset + j] + 1; + const dist = matrix[thisRowOffset + j + 1] = Math.min(rpl, del, ins); + if (dist < minDistance) + minDistance = dist; + } + // Because distance will never decrease, we can stop. There will be no + // matching child nodes. + if (minDistance > maxDistance) { + continue key; + } + } + recurse(node.get(key), query, maxDistance, results, matrix, i, n, prefix + key); + } + } +}; + +/* eslint-disable no-labels */ +/** + * A class implementing the same interface as a standard JavaScript + * [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) + * with string keys, but adding support for efficiently searching entries with + * prefix or fuzzy search. This class is used internally by {@link MiniSearch} + * as the inverted index data structure. The implementation is a radix tree + * (compressed prefix tree). + * + * Since this class can be of general utility beyond _MiniSearch_, it is + * exported by the `minisearch` package and can be imported (or required) as + * `minisearch/SearchableMap`. + * + * @typeParam T The type of the values stored in the map. + */ +class SearchableMap { + /** + * The constructor is normally called without arguments, creating an empty + * map. In order to create a {@link SearchableMap} from an iterable or from an + * object, check {@link SearchableMap.from} and {@link + * SearchableMap.fromObject}. + * + * The constructor arguments are for internal use, when creating derived + * mutable views of a map at a prefix. + */ + constructor(tree = new Map(), prefix = '') { + this._size = undefined; + this._tree = tree; + this._prefix = prefix; + } + /** + * Creates and returns a mutable view of this {@link SearchableMap}, + * containing only entries that share the given prefix. + * + * ### Usage: + * + * ```javascript + * let map = new SearchableMap() + * map.set("unicorn", 1) + * map.set("universe", 2) + * map.set("university", 3) + * map.set("unique", 4) + * map.set("hello", 5) + * + * let uni = map.atPrefix("uni") + * uni.get("unique") // => 4 + * uni.get("unicorn") // => 1 + * uni.get("hello") // => undefined + * + * let univer = map.atPrefix("univer") + * univer.get("unique") // => undefined + * univer.get("universe") // => 2 + * univer.get("university") // => 3 + * ``` + * + * @param prefix The prefix + * @return A {@link SearchableMap} representing a mutable view of the original + * Map at the given prefix + */ + atPrefix(prefix) { + if (!prefix.startsWith(this._prefix)) { + throw new Error('Mismatched prefix'); + } + const [node, path] = trackDown(this._tree, prefix.slice(this._prefix.length)); + if (node === undefined) { + const [parentNode, key] = last(path); + for (const k of parentNode.keys()) { + if (k !== LEAF && k.startsWith(key)) { + const node = new Map(); + node.set(k.slice(key.length), parentNode.get(k)); + return new SearchableMap(node, prefix); + } + } + } + return new SearchableMap(node, prefix); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear + */ + clear() { + this._size = undefined; + this._tree.clear(); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete + * @param key Key to delete + */ + delete(key) { + this._size = undefined; + return remove(this._tree, key); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries + * @return An iterator iterating through `[key, value]` entries. + */ + entries() { + return new TreeIterator(this, ENTRIES); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach + * @param fn Iteration function + */ + forEach(fn) { + for (const [key, value] of this) { + fn(key, value, this); + } + } + /** + * Returns a Map of all the entries that have a key within the given edit + * distance from the search key. The keys of the returned Map are the matching + * keys, while the values are two-element arrays where the first element is + * the value associated to the key, and the second is the edit distance of the + * key to the search key. + * + * ### Usage: + * + * ```javascript + * let map = new SearchableMap() + * map.set('hello', 'world') + * map.set('hell', 'yeah') + * map.set('ciao', 'mondo') + * + * // Get all entries that match the key 'hallo' with a maximum edit distance of 2 + * map.fuzzyGet('hallo', 2) + * // => Map(2) { 'hello' => ['world', 1], 'hell' => ['yeah', 2] } + * + * // In the example, the "hello" key has value "world" and edit distance of 1 + * // (change "e" to "a"), the key "hell" has value "yeah" and edit distance of 2 + * // (change "e" to "a", delete "o") + * ``` + * + * @param key The search key + * @param maxEditDistance The maximum edit distance (Levenshtein) + * @return A Map of the matching keys to their value and edit distance + */ + fuzzyGet(key, maxEditDistance) { + return fuzzySearch(this._tree, key, maxEditDistance); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get + * @param key Key to get + * @return Value associated to the key, or `undefined` if the key is not + * found. + */ + get(key) { + const node = lookup(this._tree, key); + return node !== undefined ? node.get(LEAF) : undefined; + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has + * @param key Key + * @return True if the key is in the map, false otherwise + */ + has(key) { + const node = lookup(this._tree, key); + return node !== undefined && node.has(LEAF); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys + * @return An `Iterable` iterating through keys + */ + keys() { + return new TreeIterator(this, KEYS); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set + * @param key Key to set + * @param value Value to associate to the key + * @return The {@link SearchableMap} itself, to allow chaining + */ + set(key, value) { + if (typeof key !== 'string') { + throw new Error('key must be a string'); + } + this._size = undefined; + const node = createPath(this._tree, key); + node.set(LEAF, value); + return this; + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size + */ + get size() { + if (this._size) { + return this._size; + } + /** @ignore */ + this._size = 0; + const iter = this.entries(); + while (!iter.next().done) + this._size += 1; + return this._size; + } + /** + * Updates the value at the given key using the provided function. The function + * is called with the current value at the key, and its return value is used as + * the new value to be set. + * + * ### Example: + * + * ```javascript + * // Increment the current value by one + * searchableMap.update('somekey', (currentValue) => currentValue == null ? 0 : currentValue + 1) + * ``` + * + * If the value at the given key is or will be an object, it might not require + * re-assignment. In that case it is better to use `fetch()`, because it is + * faster. + * + * @param key The key to update + * @param fn The function used to compute the new value from the current one + * @return The {@link SearchableMap} itself, to allow chaining + */ + update(key, fn) { + if (typeof key !== 'string') { + throw new Error('key must be a string'); + } + this._size = undefined; + const node = createPath(this._tree, key); + node.set(LEAF, fn(node.get(LEAF))); + return this; + } + /** + * Fetches the value of the given key. If the value does not exist, calls the + * given function to create a new value, which is inserted at the given key + * and subsequently returned. + * + * ### Example: + * + * ```javascript + * const map = searchableMap.fetch('somekey', () => new Map()) + * map.set('foo', 'bar') + * ``` + * + * @param key The key to update + * @param initial A function that creates a new value if the key does not exist + * @return The existing or new value at the given key + */ + fetch(key, initial) { + if (typeof key !== 'string') { + throw new Error('key must be a string'); + } + this._size = undefined; + const node = createPath(this._tree, key); + let value = node.get(LEAF); + if (value === undefined) { + node.set(LEAF, value = initial()); + } + return value; + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values + * @return An `Iterable` iterating through values. + */ + values() { + return new TreeIterator(this, VALUES); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@iterator + */ + [Symbol.iterator]() { + return this.entries(); + } + /** + * Creates a {@link SearchableMap} from an `Iterable` of entries + * + * @param entries Entries to be inserted in the {@link SearchableMap} + * @return A new {@link SearchableMap} with the given entries + */ + static from(entries) { + const tree = new SearchableMap(); + for (const [key, value] of entries) { + tree.set(key, value); + } + return tree; + } + /** + * Creates a {@link SearchableMap} from the iterable properties of a JavaScript object + * + * @param object Object of entries for the {@link SearchableMap} + * @return A new {@link SearchableMap} with the given entries + */ + static fromObject(object) { + return SearchableMap.from(Object.entries(object)); + } +} +const trackDown = (tree, key, path = []) => { + if (key.length === 0 || tree == null) { + return [tree, path]; + } + for (const k of tree.keys()) { + if (k !== LEAF && key.startsWith(k)) { + path.push([tree, k]); // performance: update in place + return trackDown(tree.get(k), key.slice(k.length), path); + } + } + path.push([tree, key]); // performance: update in place + return trackDown(undefined, '', path); +}; +const lookup = (tree, key) => { + if (key.length === 0 || tree == null) { + return tree; + } + for (const k of tree.keys()) { + if (k !== LEAF && key.startsWith(k)) { + return lookup(tree.get(k), key.slice(k.length)); + } + } +}; +// Create a path in the radix tree for the given key, and returns the deepest +// node. This function is in the hot path for indexing. It avoids unnecessary +// string operations and recursion for performance. +const createPath = (node, key) => { + const keyLength = key.length; + outer: for (let pos = 0; node && pos < keyLength;) { + for (const k of node.keys()) { + // Check whether this key is a candidate: the first characters must match. + if (k !== LEAF && key[pos] === k[0]) { + const len = Math.min(keyLength - pos, k.length); + // Advance offset to the point where key and k no longer match. + let offset = 1; + while (offset < len && key[pos + offset] === k[offset]) + ++offset; + const child = node.get(k); + if (offset === k.length) { + // The existing key is shorter than the key we need to create. + node = child; + } + else { + // Partial match: we need to insert an intermediate node to contain + // both the existing subtree and the new node. + const intermediate = new Map(); + intermediate.set(k.slice(offset), child); + node.set(key.slice(pos, pos + offset), intermediate); + node.delete(k); + node = intermediate; + } + pos += offset; + continue outer; + } + } + // Create a final child node to contain the final suffix of the key. + const child = new Map(); + node.set(key.slice(pos), child); + return child; + } + return node; +}; +const remove = (tree, key) => { + const [node, path] = trackDown(tree, key); + if (node === undefined) { + return; + } + node.delete(LEAF); + if (node.size === 0) { + cleanup(path); + } + else if (node.size === 1) { + const [key, value] = node.entries().next().value; + merge(path, key, value); + } +}; +const cleanup = (path) => { + if (path.length === 0) { + return; + } + const [node, key] = last(path); + node.delete(key); + if (node.size === 0) { + cleanup(path.slice(0, -1)); + } + else if (node.size === 1) { + const [key, value] = node.entries().next().value; + if (key !== LEAF) { + merge(path.slice(0, -1), key, value); + } + } +}; +const merge = (path, key, value) => { + if (path.length === 0) { + return; + } + const [node, nodeKey] = last(path); + node.set(nodeKey + key, value); + node.delete(nodeKey); +}; +const last = (array) => { + return array[array.length - 1]; +}; + +const OR = 'or'; +const AND = 'and'; +const AND_NOT = 'and_not'; +/** + * {@link MiniSearch} is the main entrypoint class, implementing a full-text + * search engine in memory. + * + * @typeParam T The type of the documents being indexed. + * + * ### Basic example: + * + * ```javascript + * const documents = [ + * { + * id: 1, + * title: 'Moby Dick', + * text: 'Call me Ishmael. Some years ago...', + * category: 'fiction' + * }, + * { + * id: 2, + * title: 'Zen and the Art of Motorcycle Maintenance', + * text: 'I can see by my watch...', + * category: 'fiction' + * }, + * { + * id: 3, + * title: 'Neuromancer', + * text: 'The sky above the port was...', + * category: 'fiction' + * }, + * { + * id: 4, + * title: 'Zen and the Art of Archery', + * text: 'At first sight it must seem...', + * category: 'non-fiction' + * }, + * // ...and more + * ] + * + * // Create a search engine that indexes the 'title' and 'text' fields for + * // full-text search. Search results will include 'title' and 'category' (plus the + * // id field, that is always stored and returned) + * const miniSearch = new MiniSearch({ + * fields: ['title', 'text'], + * storeFields: ['title', 'category'] + * }) + * + * // Add documents to the index + * miniSearch.addAll(documents) + * + * // Search for documents: + * let results = miniSearch.search('zen art motorcycle') + * // => [ + * // { id: 2, title: 'Zen and the Art of Motorcycle Maintenance', category: 'fiction', score: 2.77258 }, + * // { id: 4, title: 'Zen and the Art of Archery', category: 'non-fiction', score: 1.38629 } + * // ] + * ``` + */ +class MiniSearch { + /** + * @param options Configuration options + * + * ### Examples: + * + * ```javascript + * // Create a search engine that indexes the 'title' and 'text' fields of your + * // documents: + * const miniSearch = new MiniSearch({ fields: ['title', 'text'] }) + * ``` + * + * ### ID Field: + * + * ```javascript + * // Your documents are assumed to include a unique 'id' field, but if you want + * // to use a different field for document identification, you can set the + * // 'idField' option: + * const miniSearch = new MiniSearch({ idField: 'key', fields: ['title', 'text'] }) + * ``` + * + * ### Options and defaults: + * + * ```javascript + * // The full set of options (here with their default value) is: + * const miniSearch = new MiniSearch({ + * // idField: field that uniquely identifies a document + * idField: 'id', + * + * // extractField: function used to get the value of a field in a document. + * // By default, it assumes the document is a flat object with field names as + * // property keys and field values as string property values, but custom logic + * // can be implemented by setting this option to a custom extractor function. + * extractField: (document, fieldName) => document[fieldName], + * + * // tokenize: function used to split fields into individual terms. By + * // default, it is also used to tokenize search queries, unless a specific + * // `tokenize` search option is supplied. When tokenizing an indexed field, + * // the field name is passed as the second argument. + * tokenize: (string, _fieldName) => string.split(SPACE_OR_PUNCTUATION), + * + * // processTerm: function used to process each tokenized term before + * // indexing. It can be used for stemming and normalization. Return a falsy + * // value in order to discard a term. By default, it is also used to process + * // search queries, unless a specific `processTerm` option is supplied as a + * // search option. When processing a term from a indexed field, the field + * // name is passed as the second argument. + * processTerm: (term, _fieldName) => term.toLowerCase(), + * + * // searchOptions: default search options, see the `search` method for + * // details + * searchOptions: undefined, + * + * // fields: document fields to be indexed. Mandatory, but not set by default + * fields: undefined + * + * // storeFields: document fields to be stored and returned as part of the + * // search results. + * storeFields: [] + * }) + * ``` + */ + constructor(options) { + if ((options === null || options === void 0 ? void 0 : options.fields) == null) { + throw new Error('MiniSearch: option "fields" must be provided'); + } + const autoVacuum = (options.autoVacuum == null || options.autoVacuum === true) ? defaultAutoVacuumOptions : options.autoVacuum; + this._options = Object.assign(Object.assign(Object.assign({}, defaultOptions), options), { autoVacuum, searchOptions: Object.assign(Object.assign({}, defaultSearchOptions), (options.searchOptions || {})), autoSuggestOptions: Object.assign(Object.assign({}, defaultAutoSuggestOptions), (options.autoSuggestOptions || {})) }); + this._index = new SearchableMap(); + this._documentCount = 0; + this._documentIds = new Map(); + this._idToShortId = new Map(); + // Fields are defined during initialization, don't change, are few in + // number, rarely need iterating over, and have string keys. Therefore in + // this case an object is a better candidate than a Map to store the mapping + // from field key to ID. + this._fieldIds = {}; + this._fieldLength = new Map(); + this._avgFieldLength = []; + this._nextId = 0; + this._storedFields = new Map(); + this._dirtCount = 0; + this._currentVacuum = null; + this._enqueuedVacuum = null; + this._enqueuedVacuumConditions = defaultVacuumConditions; + this.addFields(this._options.fields); + } + /** + * Adds a document to the index + * + * @param document The document to be indexed + */ + add(document) { + const { extractField, tokenize, processTerm, fields, idField } = this._options; + const id = extractField(document, idField); + if (id == null) { + throw new Error(`MiniSearch: document does not have ID field "${idField}"`); + } + if (this._idToShortId.has(id)) { + throw new Error(`MiniSearch: duplicate ID ${id}`); + } + const shortDocumentId = this.addDocumentId(id); + this.saveStoredFields(shortDocumentId, document); + for (const field of fields) { + const fieldValue = extractField(document, field); + if (fieldValue == null) + continue; + const tokens = tokenize(fieldValue.toString(), field); + const fieldId = this._fieldIds[field]; + const uniqueTerms = new Set(tokens).size; + this.addFieldLength(shortDocumentId, fieldId, this._documentCount - 1, uniqueTerms); + for (const term of tokens) { + const processedTerm = processTerm(term, field); + if (Array.isArray(processedTerm)) { + for (const t of processedTerm) { + this.addTerm(fieldId, shortDocumentId, t); + } + } + else if (processedTerm) { + this.addTerm(fieldId, shortDocumentId, processedTerm); + } + } + } + } + /** + * Adds all the given documents to the index + * + * @param documents An array of documents to be indexed + */ + addAll(documents) { + for (const document of documents) + this.add(document); + } + /** + * Adds all the given documents to the index asynchronously. + * + * Returns a promise that resolves (to `undefined`) when the indexing is done. + * This method is useful when index many documents, to avoid blocking the main + * thread. The indexing is performed asynchronously and in chunks. + * + * @param documents An array of documents to be indexed + * @param options Configuration options + * @return A promise resolving to `undefined` when the indexing is done + */ + addAllAsync(documents, options = {}) { + const { chunkSize = 10 } = options; + const acc = { chunk: [], promise: Promise.resolve() }; + const { chunk, promise } = documents.reduce(({ chunk, promise }, document, i) => { + chunk.push(document); + if ((i + 1) % chunkSize === 0) { + return { + chunk: [], + promise: promise + .then(() => new Promise(resolve => setTimeout(resolve, 0))) + .then(() => this.addAll(chunk)) + }; + } + else { + return { chunk, promise }; + } + }, acc); + return promise.then(() => this.addAll(chunk)); + } + /** + * Removes the given document from the index. + * + * The document to remove must NOT have changed between indexing and removal, + * otherwise the index will be corrupted. + * + * This method requires passing the full document to be removed (not just the + * ID), and immediately removes the document from the inverted index, allowing + * memory to be released. A convenient alternative is {@link + * MiniSearch#discard}, which needs only the document ID, and has the same + * visible effect, but delays cleaning up the index until the next vacuuming. + * + * @param document The document to be removed + */ + remove(document) { + const { tokenize, processTerm, extractField, fields, idField } = this._options; + const id = extractField(document, idField); + if (id == null) { + throw new Error(`MiniSearch: document does not have ID field "${idField}"`); + } + const shortId = this._idToShortId.get(id); + if (shortId == null) { + throw new Error(`MiniSearch: cannot remove document with ID ${id}: it is not in the index`); + } + for (const field of fields) { + const fieldValue = extractField(document, field); + if (fieldValue == null) + continue; + const tokens = tokenize(fieldValue.toString(), field); + const fieldId = this._fieldIds[field]; + const uniqueTerms = new Set(tokens).size; + this.removeFieldLength(shortId, fieldId, this._documentCount, uniqueTerms); + for (const term of tokens) { + const processedTerm = processTerm(term, field); + if (Array.isArray(processedTerm)) { + for (const t of processedTerm) { + this.removeTerm(fieldId, shortId, t); + } + } + else if (processedTerm) { + this.removeTerm(fieldId, shortId, processedTerm); + } + } + } + this._storedFields.delete(shortId); + this._documentIds.delete(shortId); + this._idToShortId.delete(id); + this._fieldLength.delete(shortId); + this._documentCount -= 1; + } + /** + * Removes all the given documents from the index. If called with no arguments, + * it removes _all_ documents from the index. + * + * @param documents The documents to be removed. If this argument is omitted, + * all documents are removed. Note that, for removing all documents, it is + * more efficient to call this method with no arguments than to pass all + * documents. + */ + removeAll(documents) { + if (documents) { + for (const document of documents) + this.remove(document); + } + else if (arguments.length > 0) { + throw new Error('Expected documents to be present. Omit the argument to remove all documents.'); + } + else { + this._index = new SearchableMap(); + this._documentCount = 0; + this._documentIds = new Map(); + this._idToShortId = new Map(); + this._fieldLength = new Map(); + this._avgFieldLength = []; + this._storedFields = new Map(); + this._nextId = 0; + } + } + /** + * Discards the document with the given ID, so it won't appear in search results + * + * It has the same visible effect of {@link MiniSearch.remove} (both cause the + * document to stop appearing in searches), but a different effect on the + * internal data structures: + * + * - {@link MiniSearch#remove} requires passing the full document to be + * removed as argument, and removes it from the inverted index immediately. + * + * - {@link MiniSearch#discard} instead only needs the document ID, and + * works by marking the current version of the document as discarded, so it + * is immediately ignored by searches. This is faster and more convenient + * than {@link MiniSearch#remove}, but the index is not immediately + * modified. To take care of that, vacuuming is performed after a certain + * number of documents are discarded, cleaning up the index and allowing + * memory to be released. + * + * After discarding a document, it is possible to re-add a new version, and + * only the new version will appear in searches. In other words, discarding + * and re-adding a document works exactly like removing and re-adding it. The + * {@link MiniSearch.replace} method can also be used to replace a document + * with a new version. + * + * #### Details about vacuuming + * + * Repetite calls to this method would leave obsolete document references in + * the index, invisible to searches. Two mechanisms take care of cleaning up: + * clean up during search, and vacuuming. + * + * - Upon search, whenever a discarded ID is found (and ignored for the + * results), references to the discarded document are removed from the + * inverted index entries for the search terms. This ensures that subsequent + * searches for the same terms do not need to skip these obsolete references + * again. + * + * - In addition, vacuuming is performed automatically by default (see the + * `autoVacuum` field in {@link Options}) after a certain number of + * documents are discarded. Vacuuming traverses all terms in the index, + * cleaning up all references to discarded documents. Vacuuming can also be + * triggered manually by calling {@link MiniSearch#vacuum}. + * + * @param id The ID of the document to be discarded + */ + discard(id) { + const shortId = this._idToShortId.get(id); + if (shortId == null) { + throw new Error(`MiniSearch: cannot discard document with ID ${id}: it is not in the index`); + } + this._idToShortId.delete(id); + this._documentIds.delete(shortId); + this._storedFields.delete(shortId); + (this._fieldLength.get(shortId) || []).forEach((fieldLength, fieldId) => { + this.removeFieldLength(shortId, fieldId, this._documentCount, fieldLength); + }); + this._fieldLength.delete(shortId); + this._documentCount -= 1; + this._dirtCount += 1; + this.maybeAutoVacuum(); + } + maybeAutoVacuum() { + if (this._options.autoVacuum === false) { + return; + } + const { minDirtFactor, minDirtCount, batchSize, batchWait } = this._options.autoVacuum; + this.conditionalVacuum({ batchSize, batchWait }, { minDirtCount, minDirtFactor }); + } + /** + * Discards the documents with the given IDs, so they won't appear in search + * results + * + * It is equivalent to calling {@link MiniSearch#discard} for all the given + * IDs, but with the optimization of triggering at most one automatic + * vacuuming at the end. + * + * Note: to remove all documents from the index, it is faster and more + * convenient to call {@link MiniSearch.removeAll} with no argument, instead + * of passing all IDs to this method. + */ + discardAll(ids) { + const autoVacuum = this._options.autoVacuum; + try { + this._options.autoVacuum = false; + for (const id of ids) { + this.discard(id); + } + } + finally { + this._options.autoVacuum = autoVacuum; + } + this.maybeAutoVacuum(); + } + /** + * It replaces an existing document with the given updated version + * + * It works by discarding the current version and adding the updated one, so + * it is functionally equivalent to calling {@link MiniSearch#discard} + * followed by {@link MiniSearch#add}. The ID of the updated document should + * be the same as the original one. + * + * Since it uses {@link MiniSearch#discard} internally, this method relies on + * vacuuming to clean up obsolete document references from the index, allowing + * memory to be released (see {@link MiniSearch#discard}). + * + * @param updatedDocument The updated document to replace the old version + * with + */ + replace(updatedDocument) { + const { idField, extractField } = this._options; + const id = extractField(updatedDocument, idField); + this.discard(id); + this.add(updatedDocument); + } + /** + * Triggers a manual vacuuming, cleaning up references to discarded documents + * from the inverted index + * + * Vacuuming is only useful for applications that use the {@link + * MiniSearch#discard} or {@link MiniSearch#replace} methods. + * + * By default, vacuuming is performed automatically when needed (controlled by + * the `autoVacuum` field in {@link Options}), so there is usually no need to + * call this method, unless one wants to make sure to perform vacuuming at a + * specific moment. + * + * Vacuuming traverses all terms in the inverted index in batches, and cleans + * up references to discarded documents from the posting list, allowing memory + * to be released. + * + * The method takes an optional object as argument with the following keys: + * + * - `batchSize`: the size of each batch (1000 by default) + * + * - `batchWait`: the number of milliseconds to wait between batches (10 by + * default) + * + * On large indexes, vacuuming could have a non-negligible cost: batching + * avoids blocking the thread for long, diluting this cost so that it is not + * negatively affecting the application. Nonetheless, this method should only + * be called when necessary, and relying on automatic vacuuming is usually + * better. + * + * It returns a promise that resolves (to undefined) when the clean up is + * completed. If vacuuming is already ongoing at the time this method is + * called, a new one is enqueued immediately after the ongoing one, and a + * corresponding promise is returned. However, no more than one vacuuming is + * enqueued on top of the ongoing one, even if this method is called more + * times (enqueuing multiple ones would be useless). + * + * @param options Configuration options for the batch size and delay. See + * {@link VacuumOptions}. + */ + vacuum(options = {}) { + return this.conditionalVacuum(options); + } + conditionalVacuum(options, conditions) { + // If a vacuum is already ongoing, schedule another as soon as it finishes, + // unless there's already one enqueued. If one was already enqueued, do not + // enqueue another on top, but make sure that the conditions are the + // broadest. + if (this._currentVacuum) { + this._enqueuedVacuumConditions = this._enqueuedVacuumConditions && conditions; + if (this._enqueuedVacuum != null) { + return this._enqueuedVacuum; + } + this._enqueuedVacuum = this._currentVacuum.then(() => { + const conditions = this._enqueuedVacuumConditions; + this._enqueuedVacuumConditions = defaultVacuumConditions; + return this.performVacuuming(options, conditions); + }); + return this._enqueuedVacuum; + } + if (this.vacuumConditionsMet(conditions) === false) { + return Promise.resolve(); + } + this._currentVacuum = this.performVacuuming(options); + return this._currentVacuum; + } + performVacuuming(options, conditions) { + return __awaiter(this, void 0, void 0, function* () { + const initialDirtCount = this._dirtCount; + if (this.vacuumConditionsMet(conditions)) { + const batchSize = options.batchSize || defaultVacuumOptions.batchSize; + const batchWait = options.batchWait || defaultVacuumOptions.batchWait; + let i = 1; + for (const [term, fieldsData] of this._index) { + for (const [fieldId, fieldIndex] of fieldsData) { + for (const [shortId] of fieldIndex) { + if (this._documentIds.has(shortId)) { + continue; + } + if (fieldIndex.size <= 1) { + fieldsData.delete(fieldId); + } + else { + fieldIndex.delete(shortId); + } + } + } + if (this._index.get(term).size === 0) { + this._index.delete(term); + } + if (i % batchSize === 0) { + yield new Promise((resolve) => setTimeout(resolve, batchWait)); + } + i += 1; + } + this._dirtCount -= initialDirtCount; + } + // Make the next lines always async, so they execute after this function returns + yield null; + this._currentVacuum = this._enqueuedVacuum; + this._enqueuedVacuum = null; + }); + } + vacuumConditionsMet(conditions) { + if (conditions == null) { + return true; + } + let { minDirtCount, minDirtFactor } = conditions; + minDirtCount = minDirtCount || defaultAutoVacuumOptions.minDirtCount; + minDirtFactor = minDirtFactor || defaultAutoVacuumOptions.minDirtFactor; + return this.dirtCount >= minDirtCount && this.dirtFactor >= minDirtFactor; + } + /** + * Is `true` if a vacuuming operation is ongoing, `false` otherwise + */ + get isVacuuming() { + return this._currentVacuum != null; + } + /** + * The number of documents discarded since the most recent vacuuming + */ + get dirtCount() { + return this._dirtCount; + } + /** + * A number between 0 and 1 giving an indication about the proportion of + * documents that are discarded, and can therefore be cleaned up by vacuuming. + * A value close to 0 means that the index is relatively clean, while a higher + * value means that the index is relatively dirty, and vacuuming could release + * memory. + */ + get dirtFactor() { + return this._dirtCount / (1 + this._documentCount + this._dirtCount); + } + /** + * Returns `true` if a document with the given ID is present in the index and + * available for search, `false` otherwise + * + * @param id The document ID + */ + has(id) { + return this._idToShortId.has(id); + } + /** + * Returns the stored fields (as configured in the `storeFields` constructor + * option) for the given document ID. Returns `undefined` if the document is + * not present in the index. + * + * @param id The document ID + */ + getStoredFields(id) { + const shortId = this._idToShortId.get(id); + if (shortId == null) { + return undefined; + } + return this._storedFields.get(shortId); + } + /** + * Search for documents matching the given search query. + * + * The result is a list of scored document IDs matching the query, sorted by + * descending score, and each including data about which terms were matched and + * in which fields. + * + * ### Basic usage: + * + * ```javascript + * // Search for "zen art motorcycle" with default options: terms have to match + * // exactly, and individual terms are joined with OR + * miniSearch.search('zen art motorcycle') + * // => [ { id: 2, score: 2.77258, match: { ... } }, { id: 4, score: 1.38629, match: { ... } } ] + * ``` + * + * ### Restrict search to specific fields: + * + * ```javascript + * // Search only in the 'title' field + * miniSearch.search('zen', { fields: ['title'] }) + * ``` + * + * ### Field boosting: + * + * ```javascript + * // Boost a field + * miniSearch.search('zen', { boost: { title: 2 } }) + * ``` + * + * ### Prefix search: + * + * ```javascript + * // Search for "moto" with prefix search (it will match documents + * // containing terms that start with "moto" or "neuro") + * miniSearch.search('moto neuro', { prefix: true }) + * ``` + * + * ### Fuzzy search: + * + * ```javascript + * // Search for "ismael" with fuzzy search (it will match documents containing + * // terms similar to "ismael", with a maximum edit distance of 0.2 term.length + * // (rounded to nearest integer) + * miniSearch.search('ismael', { fuzzy: 0.2 }) + * ``` + * + * ### Combining strategies: + * + * ```javascript + * // Mix of exact match, prefix search, and fuzzy search + * miniSearch.search('ismael mob', { + * prefix: true, + * fuzzy: 0.2 + * }) + * ``` + * + * ### Advanced prefix and fuzzy search: + * + * ```javascript + * // Perform fuzzy and prefix search depending on the search term. Here + * // performing prefix and fuzzy search only on terms longer than 3 characters + * miniSearch.search('ismael mob', { + * prefix: term => term.length > 3 + * fuzzy: term => term.length > 3 ? 0.2 : null + * }) + * ``` + * + * ### Combine with AND: + * + * ```javascript + * // Combine search terms with AND (to match only documents that contain both + * // "motorcycle" and "art") + * miniSearch.search('motorcycle art', { combineWith: 'AND' }) + * ``` + * + * ### Combine with AND_NOT: + * + * There is also an AND_NOT combinator, that finds documents that match the + * first term, but do not match any of the other terms. This combinator is + * rarely useful with simple queries, and is meant to be used with advanced + * query combinations (see later for more details). + * + * ### Filtering results: + * + * ```javascript + * // Filter only results in the 'fiction' category (assuming that 'category' + * // is a stored field) + * miniSearch.search('motorcycle art', { + * filter: (result) => result.category === 'fiction' + * }) + * ``` + * + * ### Wildcard query + * + * Searching for an empty string (assuming the default tokenizer) returns no + * results. Sometimes though, one needs to match all documents, like in a + * "wildcard" search. This is possible by passing the special value + * {@link MiniSearch.wildcard} as the query: + * + * ```javascript + * // Return search results for all documents + * miniSearch.search(MiniSearch.wildcard) + * ``` + * + * Note that search options such as `filter` and `boostDocument` are still + * applied, influencing which results are returned, and their order: + * + * ```javascript + * // Return search results for all documents in the 'fiction' category + * miniSearch.search(MiniSearch.wildcard, { + * filter: (result) => result.category === 'fiction' + * }) + * ``` + * + * ### Advanced combination of queries: + * + * It is possible to combine different subqueries with OR, AND, and AND_NOT, + * and even with different search options, by passing a query expression + * tree object as the first argument, instead of a string. + * + * ```javascript + * // Search for documents that contain "zen" and ("motorcycle" or "archery") + * miniSearch.search({ + * combineWith: 'AND', + * queries: [ + * 'zen', + * { + * combineWith: 'OR', + * queries: ['motorcycle', 'archery'] + * } + * ] + * }) + * + * // Search for documents that contain ("apple" or "pear") but not "juice" and + * // not "tree" + * miniSearch.search({ + * combineWith: 'AND_NOT', + * queries: [ + * { + * combineWith: 'OR', + * queries: ['apple', 'pear'] + * }, + * 'juice', + * 'tree' + * ] + * }) + * ``` + * + * Each node in the expression tree can be either a string, or an object that + * supports all {@link SearchOptions} fields, plus a `queries` array field for + * subqueries. + * + * Note that, while this can become complicated to do by hand for complex or + * deeply nested queries, it provides a formalized expression tree API for + * external libraries that implement a parser for custom query languages. + * + * @param query Search query + * @param searchOptions Search options. Each option, if not given, defaults to the corresponding value of `searchOptions` given to the constructor, or to the library default. + */ + search(query, searchOptions = {}) { + const { searchOptions: globalSearchOptions } = this._options; + const searchOptionsWithDefaults = Object.assign(Object.assign({}, globalSearchOptions), searchOptions); + const rawResults = this.executeQuery(query, searchOptions); + const results = []; + for (const [docId, { score, terms, match }] of rawResults) { + // terms are the matched query terms, which will be returned to the user + // as queryTerms. The quality is calculated based on them, as opposed to + // the matched terms in the document (which can be different due to + // prefix and fuzzy match) + const quality = terms.length || 1; + const result = { + id: this._documentIds.get(docId), + score: score * quality, + terms: Object.keys(match), + queryTerms: terms, + match + }; + Object.assign(result, this._storedFields.get(docId)); + if (searchOptionsWithDefaults.filter == null || searchOptionsWithDefaults.filter(result)) { + results.push(result); + } + } + // If it's a wildcard query, and no document boost is applied, skip sorting + // the results, as all results have the same score of 1 + if (query === MiniSearch.wildcard && searchOptionsWithDefaults.boostDocument == null) { + return results; + } + results.sort(byScore); + return results; + } + /** + * Provide suggestions for the given search query + * + * The result is a list of suggested modified search queries, derived from the + * given search query, each with a relevance score, sorted by descending score. + * + * By default, it uses the same options used for search, except that by + * default it performs prefix search on the last term of the query, and + * combine terms with `'AND'` (requiring all query terms to match). Custom + * options can be passed as a second argument. Defaults can be changed upon + * calling the {@link MiniSearch} constructor, by passing a + * `autoSuggestOptions` option. + * + * ### Basic usage: + * + * ```javascript + * // Get suggestions for 'neuro': + * miniSearch.autoSuggest('neuro') + * // => [ { suggestion: 'neuromancer', terms: [ 'neuromancer' ], score: 0.46240 } ] + * ``` + * + * ### Multiple words: + * + * ```javascript + * // Get suggestions for 'zen ar': + * miniSearch.autoSuggest('zen ar') + * // => [ + * // { suggestion: 'zen archery art', terms: [ 'zen', 'archery', 'art' ], score: 1.73332 }, + * // { suggestion: 'zen art', terms: [ 'zen', 'art' ], score: 1.21313 } + * // ] + * ``` + * + * ### Fuzzy suggestions: + * + * ```javascript + * // Correct spelling mistakes using fuzzy search: + * miniSearch.autoSuggest('neromancer', { fuzzy: 0.2 }) + * // => [ { suggestion: 'neuromancer', terms: [ 'neuromancer' ], score: 1.03998 } ] + * ``` + * + * ### Filtering: + * + * ```javascript + * // Get suggestions for 'zen ar', but only within the 'fiction' category + * // (assuming that 'category' is a stored field): + * miniSearch.autoSuggest('zen ar', { + * filter: (result) => result.category === 'fiction' + * }) + * // => [ + * // { suggestion: 'zen archery art', terms: [ 'zen', 'archery', 'art' ], score: 1.73332 }, + * // { suggestion: 'zen art', terms: [ 'zen', 'art' ], score: 1.21313 } + * // ] + * ``` + * + * @param queryString Query string to be expanded into suggestions + * @param options Search options. The supported options and default values + * are the same as for the {@link MiniSearch#search} method, except that by + * default prefix search is performed on the last term in the query, and terms + * are combined with `'AND'`. + * @return A sorted array of suggestions sorted by relevance score. + */ + autoSuggest(queryString, options = {}) { + options = Object.assign(Object.assign({}, this._options.autoSuggestOptions), options); + const suggestions = new Map(); + for (const { score, terms } of this.search(queryString, options)) { + const phrase = terms.join(' '); + const suggestion = suggestions.get(phrase); + if (suggestion != null) { + suggestion.score += score; + suggestion.count += 1; + } + else { + suggestions.set(phrase, { score, terms, count: 1 }); + } + } + const results = []; + for (const [suggestion, { score, terms, count }] of suggestions) { + results.push({ suggestion, terms, score: score / count }); + } + results.sort(byScore); + return results; + } + /** + * Total number of documents available to search + */ + get documentCount() { + return this._documentCount; + } + /** + * Number of terms in the index + */ + get termCount() { + return this._index.size; + } + /** + * Deserializes a JSON index (serialized with `JSON.stringify(miniSearch)`) + * and instantiates a MiniSearch instance. It should be given the same options + * originally used when serializing the index. + * + * ### Usage: + * + * ```javascript + * // If the index was serialized with: + * let miniSearch = new MiniSearch({ fields: ['title', 'text'] }) + * miniSearch.addAll(documents) + * + * const json = JSON.stringify(miniSearch) + * // It can later be deserialized like this: + * miniSearch = MiniSearch.loadJSON(json, { fields: ['title', 'text'] }) + * ``` + * + * @param json JSON-serialized index + * @param options configuration options, same as the constructor + * @return An instance of MiniSearch deserialized from the given JSON. + */ + static loadJSON(json, options) { + if (options == null) { + throw new Error('MiniSearch: loadJSON should be given the same options used when serializing the index'); + } + return this.loadJS(JSON.parse(json), options); + } + /** + * Async equivalent of {@link MiniSearch.loadJSON} + * + * This function is an alternative to {@link MiniSearch.loadJSON} that returns + * a promise, and loads the index in batches, leaving pauses between them to avoid + * blocking the main thread. It tends to be slower than the synchronous + * version, but does not block the main thread, so it can be a better choice + * when deserializing very large indexes. + * + * @param json JSON-serialized index + * @param options configuration options, same as the constructor + * @return A Promise that will resolve to an instance of MiniSearch deserialized from the given JSON. + */ + static loadJSONAsync(json, options) { + return __awaiter(this, void 0, void 0, function* () { + if (options == null) { + throw new Error('MiniSearch: loadJSON should be given the same options used when serializing the index'); + } + return this.loadJSAsync(JSON.parse(json), options); + }); + } + /** + * Returns the default value of an option. It will throw an error if no option + * with the given name exists. + * + * @param optionName Name of the option + * @return The default value of the given option + * + * ### Usage: + * + * ```javascript + * // Get default tokenizer + * MiniSearch.getDefault('tokenize') + * + * // Get default term processor + * MiniSearch.getDefault('processTerm') + * + * // Unknown options will throw an error + * MiniSearch.getDefault('notExisting') + * // => throws 'MiniSearch: unknown option "notExisting"' + * ``` + */ + static getDefault(optionName) { + if (defaultOptions.hasOwnProperty(optionName)) { + return getOwnProperty(defaultOptions, optionName); + } + else { + throw new Error(`MiniSearch: unknown option "${optionName}"`); + } + } + /** + * @ignore + */ + static loadJS(js, options) { + const { index, documentIds, fieldLength, storedFields, serializationVersion } = js; + const miniSearch = this.instantiateMiniSearch(js, options); + miniSearch._documentIds = objectToNumericMap(documentIds); + miniSearch._fieldLength = objectToNumericMap(fieldLength); + miniSearch._storedFields = objectToNumericMap(storedFields); + for (const [shortId, id] of miniSearch._documentIds) { + miniSearch._idToShortId.set(id, shortId); + } + for (const [term, data] of index) { + const dataMap = new Map(); + for (const fieldId of Object.keys(data)) { + let indexEntry = data[fieldId]; + // Version 1 used to nest the index entry inside a field called ds + if (serializationVersion === 1) { + indexEntry = indexEntry.ds; + } + dataMap.set(parseInt(fieldId, 10), objectToNumericMap(indexEntry)); + } + miniSearch._index.set(term, dataMap); + } + return miniSearch; + } + /** + * @ignore + */ + static loadJSAsync(js, options) { + return __awaiter(this, void 0, void 0, function* () { + const { index, documentIds, fieldLength, storedFields, serializationVersion } = js; + const miniSearch = this.instantiateMiniSearch(js, options); + miniSearch._documentIds = yield objectToNumericMapAsync(documentIds); + miniSearch._fieldLength = yield objectToNumericMapAsync(fieldLength); + miniSearch._storedFields = yield objectToNumericMapAsync(storedFields); + for (const [shortId, id] of miniSearch._documentIds) { + miniSearch._idToShortId.set(id, shortId); + } + let count = 0; + for (const [term, data] of index) { + const dataMap = new Map(); + for (const fieldId of Object.keys(data)) { + let indexEntry = data[fieldId]; + // Version 1 used to nest the index entry inside a field called ds + if (serializationVersion === 1) { + indexEntry = indexEntry.ds; + } + dataMap.set(parseInt(fieldId, 10), yield objectToNumericMapAsync(indexEntry)); + } + if (++count % 1000 === 0) + yield wait(0); + miniSearch._index.set(term, dataMap); + } + return miniSearch; + }); + } + /** + * @ignore + */ + static instantiateMiniSearch(js, options) { + const { documentCount, nextId, fieldIds, averageFieldLength, dirtCount, serializationVersion } = js; + if (serializationVersion !== 1 && serializationVersion !== 2) { + throw new Error('MiniSearch: cannot deserialize an index created with an incompatible version'); + } + const miniSearch = new MiniSearch(options); + miniSearch._documentCount = documentCount; + miniSearch._nextId = nextId; + miniSearch._idToShortId = new Map(); + miniSearch._fieldIds = fieldIds; + miniSearch._avgFieldLength = averageFieldLength; + miniSearch._dirtCount = dirtCount || 0; + miniSearch._index = new SearchableMap(); + return miniSearch; + } + /** + * @ignore + */ + executeQuery(query, searchOptions = {}) { + if (query === MiniSearch.wildcard) { + return this.executeWildcardQuery(searchOptions); + } + if (typeof query !== 'string') { + const options = Object.assign(Object.assign(Object.assign({}, searchOptions), query), { queries: undefined }); + const results = query.queries.map((subquery) => this.executeQuery(subquery, options)); + return this.combineResults(results, options.combineWith); + } + const { tokenize, processTerm, searchOptions: globalSearchOptions } = this._options; + const options = Object.assign(Object.assign({ tokenize, processTerm }, globalSearchOptions), searchOptions); + const { tokenize: searchTokenize, processTerm: searchProcessTerm } = options; + const terms = searchTokenize(query) + .flatMap((term) => searchProcessTerm(term)) + .filter((term) => !!term); + const queries = terms.map(termToQuerySpec(options)); + const results = queries.map(query => this.executeQuerySpec(query, options)); + return this.combineResults(results, options.combineWith); + } + /** + * @ignore + */ + executeQuerySpec(query, searchOptions) { + const options = Object.assign(Object.assign({}, this._options.searchOptions), searchOptions); + const boosts = (options.fields || this._options.fields).reduce((boosts, field) => (Object.assign(Object.assign({}, boosts), { [field]: getOwnProperty(options.boost, field) || 1 })), {}); + const { boostDocument, weights, maxFuzzy, bm25: bm25params } = options; + const { fuzzy: fuzzyWeight, prefix: prefixWeight } = Object.assign(Object.assign({}, defaultSearchOptions.weights), weights); + const data = this._index.get(query.term); + const results = this.termResults(query.term, query.term, 1, query.termBoost, data, boosts, boostDocument, bm25params); + let prefixMatches; + let fuzzyMatches; + if (query.prefix) { + prefixMatches = this._index.atPrefix(query.term); + } + if (query.fuzzy) { + const fuzzy = (query.fuzzy === true) ? 0.2 : query.fuzzy; + const maxDistance = fuzzy < 1 ? Math.min(maxFuzzy, Math.round(query.term.length * fuzzy)) : fuzzy; + if (maxDistance) + fuzzyMatches = this._index.fuzzyGet(query.term, maxDistance); + } + if (prefixMatches) { + for (const [term, data] of prefixMatches) { + const distance = term.length - query.term.length; + if (!distance) { + continue; + } // Skip exact match. + // Delete the term from fuzzy results (if present) if it is also a + // prefix result. This entry will always be scored as a prefix result. + fuzzyMatches === null || fuzzyMatches === void 0 ? void 0 : fuzzyMatches.delete(term); + // Weight gradually approaches 0 as distance goes to infinity, with the + // weight for the hypothetical distance 0 being equal to prefixWeight. + // The rate of change is much lower than that of fuzzy matches to + // account for the fact that prefix matches stay more relevant than + // fuzzy matches for longer distances. + const weight = prefixWeight * term.length / (term.length + 0.3 * distance); + this.termResults(query.term, term, weight, query.termBoost, data, boosts, boostDocument, bm25params, results); + } + } + if (fuzzyMatches) { + for (const term of fuzzyMatches.keys()) { + const [data, distance] = fuzzyMatches.get(term); + if (!distance) { + continue; + } // Skip exact match. + // Weight gradually approaches 0 as distance goes to infinity, with the + // weight for the hypothetical distance 0 being equal to fuzzyWeight. + const weight = fuzzyWeight * term.length / (term.length + distance); + this.termResults(query.term, term, weight, query.termBoost, data, boosts, boostDocument, bm25params, results); + } + } + return results; + } + /** + * @ignore + */ + executeWildcardQuery(searchOptions) { + const results = new Map(); + const options = Object.assign(Object.assign({}, this._options.searchOptions), searchOptions); + for (const [shortId, id] of this._documentIds) { + const score = options.boostDocument ? options.boostDocument(id, '', this._storedFields.get(shortId)) : 1; + results.set(shortId, { + score, + terms: [], + match: {} + }); + } + return results; + } + /** + * @ignore + */ + combineResults(results, combineWith = OR) { + if (results.length === 0) { + return new Map(); + } + const operator = combineWith.toLowerCase(); + const combinator = combinators[operator]; + if (!combinator) { + throw new Error(`Invalid combination operator: ${combineWith}`); + } + return results.reduce(combinator) || new Map(); + } + /** + * Allows serialization of the index to JSON, to possibly store it and later + * deserialize it with {@link MiniSearch.loadJSON}. + * + * Normally one does not directly call this method, but rather call the + * standard JavaScript `JSON.stringify()` passing the {@link MiniSearch} + * instance, and JavaScript will internally call this method. Upon + * deserialization, one must pass to {@link MiniSearch.loadJSON} the same + * options used to create the original instance that was serialized. + * + * ### Usage: + * + * ```javascript + * // Serialize the index: + * let miniSearch = new MiniSearch({ fields: ['title', 'text'] }) + * miniSearch.addAll(documents) + * const json = JSON.stringify(miniSearch) + * + * // Later, to deserialize it: + * miniSearch = MiniSearch.loadJSON(json, { fields: ['title', 'text'] }) + * ``` + * + * @return A plain-object serializable representation of the search index. + */ + toJSON() { + const index = []; + for (const [term, fieldIndex] of this._index) { + const data = {}; + for (const [fieldId, freqs] of fieldIndex) { + data[fieldId] = Object.fromEntries(freqs); + } + index.push([term, data]); + } + return { + documentCount: this._documentCount, + nextId: this._nextId, + documentIds: Object.fromEntries(this._documentIds), + fieldIds: this._fieldIds, + fieldLength: Object.fromEntries(this._fieldLength), + averageFieldLength: this._avgFieldLength, + storedFields: Object.fromEntries(this._storedFields), + dirtCount: this._dirtCount, + index, + serializationVersion: 2 + }; + } + /** + * @ignore + */ + termResults(sourceTerm, derivedTerm, termWeight, termBoost, fieldTermData, fieldBoosts, boostDocumentFn, bm25params, results = new Map()) { + if (fieldTermData == null) + return results; + for (const field of Object.keys(fieldBoosts)) { + const fieldBoost = fieldBoosts[field]; + const fieldId = this._fieldIds[field]; + const fieldTermFreqs = fieldTermData.get(fieldId); + if (fieldTermFreqs == null) + continue; + let matchingFields = fieldTermFreqs.size; + const avgFieldLength = this._avgFieldLength[fieldId]; + for (const docId of fieldTermFreqs.keys()) { + if (!this._documentIds.has(docId)) { + this.removeTerm(fieldId, docId, derivedTerm); + matchingFields -= 1; + continue; + } + const docBoost = boostDocumentFn ? boostDocumentFn(this._documentIds.get(docId), derivedTerm, this._storedFields.get(docId)) : 1; + if (!docBoost) + continue; + const termFreq = fieldTermFreqs.get(docId); + const fieldLength = this._fieldLength.get(docId)[fieldId]; + // NOTE: The total number of fields is set to the number of documents + // `this._documentCount`. It could also make sense to use the number of + // documents where the current field is non-blank as a normalization + // factor. This will make a difference in scoring if the field is rarely + // present. This is currently not supported, and may require further + // analysis to see if it is a valid use case. + const rawScore = calcBM25Score(termFreq, matchingFields, this._documentCount, fieldLength, avgFieldLength, bm25params); + const weightedScore = termWeight * termBoost * fieldBoost * docBoost * rawScore; + const result = results.get(docId); + if (result) { + result.score += weightedScore; + assignUniqueTerm(result.terms, sourceTerm); + const match = getOwnProperty(result.match, derivedTerm); + if (match) { + match.push(field); + } + else { + result.match[derivedTerm] = [field]; + } + } + else { + results.set(docId, { + score: weightedScore, + terms: [sourceTerm], + match: { [derivedTerm]: [field] } + }); + } + } + } + return results; + } + /** + * @ignore + */ + addTerm(fieldId, documentId, term) { + const indexData = this._index.fetch(term, createMap); + let fieldIndex = indexData.get(fieldId); + if (fieldIndex == null) { + fieldIndex = new Map(); + fieldIndex.set(documentId, 1); + indexData.set(fieldId, fieldIndex); + } + else { + const docs = fieldIndex.get(documentId); + fieldIndex.set(documentId, (docs || 0) + 1); + } + } + /** + * @ignore + */ + removeTerm(fieldId, documentId, term) { + if (!this._index.has(term)) { + this.warnDocumentChanged(documentId, fieldId, term); + return; + } + const indexData = this._index.fetch(term, createMap); + const fieldIndex = indexData.get(fieldId); + if (fieldIndex == null || fieldIndex.get(documentId) == null) { + this.warnDocumentChanged(documentId, fieldId, term); + } + else if (fieldIndex.get(documentId) <= 1) { + if (fieldIndex.size <= 1) { + indexData.delete(fieldId); + } + else { + fieldIndex.delete(documentId); + } + } + else { + fieldIndex.set(documentId, fieldIndex.get(documentId) - 1); + } + if (this._index.get(term).size === 0) { + this._index.delete(term); + } + } + /** + * @ignore + */ + warnDocumentChanged(shortDocumentId, fieldId, term) { + for (const fieldName of Object.keys(this._fieldIds)) { + if (this._fieldIds[fieldName] === fieldId) { + this._options.logger('warn', `MiniSearch: document with ID ${this._documentIds.get(shortDocumentId)} has changed before removal: term "${term}" was not present in field "${fieldName}". Removing a document after it has changed can corrupt the index!`, 'version_conflict'); + return; + } + } + } + /** + * @ignore + */ + addDocumentId(documentId) { + const shortDocumentId = this._nextId; + this._idToShortId.set(documentId, shortDocumentId); + this._documentIds.set(shortDocumentId, documentId); + this._documentCount += 1; + this._nextId += 1; + return shortDocumentId; + } + /** + * @ignore + */ + addFields(fields) { + for (let i = 0; i < fields.length; i++) { + this._fieldIds[fields[i]] = i; + } + } + /** + * @ignore + */ + addFieldLength(documentId, fieldId, count, length) { + let fieldLengths = this._fieldLength.get(documentId); + if (fieldLengths == null) + this._fieldLength.set(documentId, fieldLengths = []); + fieldLengths[fieldId] = length; + const averageFieldLength = this._avgFieldLength[fieldId] || 0; + const totalFieldLength = (averageFieldLength * count) + length; + this._avgFieldLength[fieldId] = totalFieldLength / (count + 1); + } + /** + * @ignore + */ + removeFieldLength(documentId, fieldId, count, length) { + if (count === 1) { + this._avgFieldLength[fieldId] = 0; + return; + } + const totalFieldLength = (this._avgFieldLength[fieldId] * count) - length; + this._avgFieldLength[fieldId] = totalFieldLength / (count - 1); + } + /** + * @ignore + */ + saveStoredFields(documentId, doc) { + const { storeFields, extractField } = this._options; + if (storeFields == null || storeFields.length === 0) { + return; + } + let documentFields = this._storedFields.get(documentId); + if (documentFields == null) + this._storedFields.set(documentId, documentFields = {}); + for (const fieldName of storeFields) { + const fieldValue = extractField(doc, fieldName); + if (fieldValue !== undefined) + documentFields[fieldName] = fieldValue; + } + } +} +/** + * The special wildcard symbol that can be passed to {@link MiniSearch#search} + * to match all documents + */ +MiniSearch.wildcard = Symbol('*'); +const getOwnProperty = (object, property) => Object.prototype.hasOwnProperty.call(object, property) ? object[property] : undefined; +const combinators = { + [OR]: (a, b) => { + for (const docId of b.keys()) { + const existing = a.get(docId); + if (existing == null) { + a.set(docId, b.get(docId)); + } + else { + const { score, terms, match } = b.get(docId); + existing.score = existing.score + score; + existing.match = Object.assign(existing.match, match); + assignUniqueTerms(existing.terms, terms); + } + } + return a; + }, + [AND]: (a, b) => { + const combined = new Map(); + for (const docId of b.keys()) { + const existing = a.get(docId); + if (existing == null) + continue; + const { score, terms, match } = b.get(docId); + assignUniqueTerms(existing.terms, terms); + combined.set(docId, { + score: existing.score + score, + terms: existing.terms, + match: Object.assign(existing.match, match) + }); + } + return combined; + }, + [AND_NOT]: (a, b) => { + for (const docId of b.keys()) + a.delete(docId); + return a; + } +}; +const defaultBM25params = { k: 1.2, b: 0.7, d: 0.5 }; +const calcBM25Score = (termFreq, matchingCount, totalCount, fieldLength, avgFieldLength, bm25params) => { + const { k, b, d } = bm25params; + const invDocFreq = Math.log(1 + (totalCount - matchingCount + 0.5) / (matchingCount + 0.5)); + return invDocFreq * (d + termFreq * (k + 1) / (termFreq + k * (1 - b + b * fieldLength / avgFieldLength))); +}; +const termToQuerySpec = (options) => (term, i, terms) => { + const fuzzy = (typeof options.fuzzy === 'function') + ? options.fuzzy(term, i, terms) + : (options.fuzzy || false); + const prefix = (typeof options.prefix === 'function') + ? options.prefix(term, i, terms) + : (options.prefix === true); + const termBoost = (typeof options.boostTerm === 'function') + ? options.boostTerm(term, i, terms) + : 1; + return { term, fuzzy, prefix, termBoost }; +}; +const defaultOptions = { + idField: 'id', + extractField: (document, fieldName) => document[fieldName], + tokenize: (text) => text.split(SPACE_OR_PUNCTUATION), + processTerm: (term) => term.toLowerCase(), + fields: undefined, + searchOptions: undefined, + storeFields: [], + logger: (level, message) => { + if (typeof (console === null || console === void 0 ? void 0 : console[level]) === 'function') + console[level](message); + }, + autoVacuum: true +}; +const defaultSearchOptions = { + combineWith: OR, + prefix: false, + fuzzy: false, + maxFuzzy: 6, + boost: {}, + weights: { fuzzy: 0.45, prefix: 0.375 }, + bm25: defaultBM25params +}; +const defaultAutoSuggestOptions = { + combineWith: AND, + prefix: (term, i, terms) => i === terms.length - 1 +}; +const defaultVacuumOptions = { batchSize: 1000, batchWait: 10 }; +const defaultVacuumConditions = { minDirtFactor: 0.1, minDirtCount: 20 }; +const defaultAutoVacuumOptions = Object.assign(Object.assign({}, defaultVacuumOptions), defaultVacuumConditions); +const assignUniqueTerm = (target, term) => { + // Avoid adding duplicate terms. + if (!target.includes(term)) + target.push(term); +}; +const assignUniqueTerms = (target, source) => { + for (const term of source) { + // Avoid adding duplicate terms. + if (!target.includes(term)) + target.push(term); + } +}; +const byScore = ({ score: a }, { score: b }) => b - a; +const createMap = () => new Map(); +const objectToNumericMap = (object) => { + const map = new Map(); + for (const key of Object.keys(object)) { + map.set(parseInt(key, 10), object[key]); + } + return map; +}; +const objectToNumericMapAsync = (object) => __awaiter(void 0, void 0, void 0, function* () { + const map = new Map(); + let count = 0; + for (const key of Object.keys(object)) { + map.set(parseInt(key, 10), object[key]); + if (++count % 1000 === 0) { + yield wait(0); + } + } + return map; +}); +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +// This regular expression matches any Unicode space, newline, or punctuation +// character +const SPACE_OR_PUNCTUATION = /[\n\r\p{Z}\p{P}]+/u; + +export { MiniSearch as default }; +//# sourceMappingURL=index.js.map diff --git a/libs/tiny-segmenter.js b/libs/tiny-segmenter.js new file mode 100644 index 0000000..121c1ea --- /dev/null +++ b/libs/tiny-segmenter.js @@ -0,0 +1,177 @@ +// TinySegmenter 0.1 -- Super compact Japanese tokenizer in Javascript +// (c) 2008 Taku Kudo +// TinySegmenter is freely distributable under the terms of a new BSD licence. +// For details, see http://chasen.org/~taku/software/TinySegmenter/LICENCE.txt + +function TinySegmenter() { + var patterns = { + "[一二三四五六七八九十百千万億兆]":"M", + "[一-龠々〆ヵヶ]":"H", + "[ぁ-ん]":"I", + "[ァ-ヴーア-ン゙ー]":"K", + "[a-zA-Za-zA-Z]":"A", + "[0-90-9]":"N" + } + this.chartype_ = []; + for (var i in patterns) { + var regexp = new RegExp; + regexp.compile(i) + this.chartype_.push([regexp, patterns[i]]); + } + + this.BIAS__ = -332 + this.BC1__ = {"HH":6,"II":2461,"KH":406,"OH":-1378}; + this.BC2__ = {"AA":-3267,"AI":2744,"AN":-878,"HH":-4070,"HM":-1711,"HN":4012,"HO":3761,"IA":1327,"IH":-1184,"II":-1332,"IK":1721,"IO":5492,"KI":3831,"KK":-8741,"MH":-3132,"MK":3334,"OO":-2920}; + this.BC3__ = {"HH":996,"HI":626,"HK":-721,"HN":-1307,"HO":-836,"IH":-301,"KK":2762,"MK":1079,"MM":4034,"OA":-1652,"OH":266}; + this.BP1__ = {"BB":295,"OB":304,"OO":-125,"UB":352}; + this.BP2__ = {"BO":60,"OO":-1762}; + this.BQ1__ = {"BHH":1150,"BHM":1521,"BII":-1158,"BIM":886,"BMH":1208,"BNH":449,"BOH":-91,"BOO":-2597,"OHI":451,"OIH":-296,"OKA":1851,"OKH":-1020,"OKK":904,"OOO":2965}; + this.BQ2__ = {"BHH":118,"BHI":-1159,"BHM":466,"BIH":-919,"BKK":-1720,"BKO":864,"OHH":-1139,"OHM":-181,"OIH":153,"UHI":-1146}; + this.BQ3__ = {"BHH":-792,"BHI":2664,"BII":-299,"BKI":419,"BMH":937,"BMM":8335,"BNN":998,"BOH":775,"OHH":2174,"OHM":439,"OII":280,"OKH":1798,"OKI":-793,"OKO":-2242,"OMH":-2402,"OOO":11699}; + this.BQ4__ = {"BHH":-3895,"BIH":3761,"BII":-4654,"BIK":1348,"BKK":-1806,"BMI":-3385,"BOO":-12396,"OAH":926,"OHH":266,"OHK":-2036,"ONN":-973}; + this.BW1__ = {",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682}; + this.BW2__ = {"..":-11822,"11":-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669}; + this.BW3__ = {"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1000,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990}; + this.TC1__ = {"AAA":1093,"HHH":1029,"HHM":580,"HII":998,"HOH":-390,"HOM":-331,"IHI":1169,"IOH":-142,"IOI":-1015,"IOM":467,"MMH":187,"OOI":-1832}; + this.TC2__ = {"HHO":2088,"HII":-1023,"HMM":-1154,"IHI":-1965,"KKH":703,"OII":-2649}; + this.TC3__ = {"AAA":-294,"HHH":346,"HHI":-341,"HII":-1088,"HIK":731,"HOH":-1486,"IHH":128,"IHI":-3041,"IHO":-1935,"IIH":-825,"IIM":-1035,"IOI":-542,"KHH":-1216,"KKA":491,"KKH":-1217,"KOK":-1009,"MHH":-2694,"MHM":-457,"MHO":123,"MMH":-471,"NNH":-1689,"NNO":662,"OHO":-3393}; + this.TC4__ = {"HHH":-203,"HHI":1344,"HHK":365,"HHM":-122,"HHN":182,"HHO":669,"HIH":804,"HII":679,"HOH":446,"IHH":695,"IHO":-2324,"IIH":321,"III":1497,"IIO":656,"IOO":54,"KAK":4845,"KKA":3386,"KKK":3065,"MHH":-405,"MHI":201,"MMH":-241,"MMM":661,"MOM":841}; + this.TQ1__ = {"BHHH":-227,"BHHI":316,"BHIH":-132,"BIHH":60,"BIII":1595,"BNHH":-744,"BOHH":225,"BOOO":-908,"OAKK":482,"OHHH":281,"OHIH":249,"OIHI":200,"OIIH":-68}; + this.TQ2__ = {"BIHH":-1401,"BIII":-1033,"BKAK":-543,"BOOO":-5591}; + this.TQ3__ = {"BHHH":478,"BHHM":-1073,"BHIH":222,"BHII":-504,"BIIH":-116,"BIII":-105,"BMHI":-863,"BMHM":-464,"BOMH":620,"OHHH":346,"OHHI":1729,"OHII":997,"OHMH":481,"OIHH":623,"OIIH":1344,"OKAK":2792,"OKHH":587,"OKKA":679,"OOHH":110,"OOII":-685}; + this.TQ4__ = {"BHHH":-721,"BHHM":-3604,"BHII":-966,"BIIH":-607,"BIII":-2181,"OAAA":-2763,"OAKK":180,"OHHH":-294,"OHHI":2446,"OHHO":480,"OHIH":-1573,"OIHH":1935,"OIHI":-493,"OIIH":626,"OIII":-4007,"OKAK":-8156}; + this.TW1__ = {"につい":-4681,"東京都":2026}; + this.TW2__ = {"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216}; + this.TW3__ = {"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287}; + this.TW4__ = {"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865}; + this.UC1__ = {"A":484,"K":93,"M":645,"O":-505}; + this.UC2__ = {"A":819,"H":1059,"I":409,"M":3987,"N":5775,"O":646}; + this.UC3__ = {"A":-1370,"I":2311}; + this.UC4__ = {"A":-2643,"H":1809,"I":-1032,"K":-3450,"M":3565,"N":3876,"O":6646}; + this.UC5__ = {"H":313,"I":-1238,"K":-799,"M":539,"O":-831}; + this.UC6__ = {"H":-506,"I":-253,"K":87,"M":247,"O":-387}; + this.UP1__ = {"O":-214}; + this.UP2__ = {"B":69,"O":935}; + this.UP3__ = {"B":189}; + this.UQ1__ = {"BH":21,"BI":-12,"BK":-99,"BN":142,"BO":-56,"OH":-95,"OI":477,"OK":410,"OO":-2422}; + this.UQ2__ = {"BH":216,"BI":113,"OK":1759}; + this.UQ3__ = {"BA":-479,"BH":42,"BI":1913,"BK":-7198,"BM":3160,"BN":6427,"BO":14761,"OI":-827,"ON":-3212}; + this.UW1__ = {",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135}; + this.UW2__ = {",":-829,"、":-829,"〇":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568}; + this.UW3__ = {",":4889,"1":-800,"−":-1723,"、":4889,"々":-2311,"〇":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"1":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278}; + this.UW4__ = {",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"〇":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1000,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637}; + this.UW5__ = {",":465,".":-299,"1":-514,"E2":-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"1":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343}; + this.UW6__ = {",":227,".":808,"1":-270,"E1":306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"1":-270,"E1":306,"ル":-673,"ン":-496}; + + return this; +} + +TinySegmenter.prototype.ctype_ = function(str) { + for (var i in this.chartype_) { + if (str.match(this.chartype_[i][0])) { + return this.chartype_[i][1]; + } + } + return "O"; +} + +TinySegmenter.prototype.ts_ = function(v) { + if (v) { return v; } + return 0; +} + +TinySegmenter.prototype.segment = function(input) { + if (input == null || input == undefined || input == "") { + return []; + } + var result = []; + var seg = ["B3","B2","B1"]; + var ctype = ["O","O","O"]; + var o = input.split(""); + for (i = 0; i < o.length; ++i) { + seg.push(o[i]); + ctype.push(this.ctype_(o[i])) + } + seg.push("E1"); + seg.push("E2"); + seg.push("E3"); + ctype.push("O"); + ctype.push("O"); + ctype.push("O"); + var word = seg[3]; + var p1 = "U"; + var p2 = "U"; + var p3 = "U"; + for (var i = 4; i < seg.length - 3; ++i) { + var score = this.BIAS__; + var w1 = seg[i-3]; + var w2 = seg[i-2]; + var w3 = seg[i-1]; + var w4 = seg[i]; + var w5 = seg[i+1]; + var w6 = seg[i+2]; + var c1 = ctype[i-3]; + var c2 = ctype[i-2]; + var c3 = ctype[i-1]; + var c4 = ctype[i]; + var c5 = ctype[i+1]; + var c6 = ctype[i+2]; + score += this.ts_(this.UP1__[p1]); + score += this.ts_(this.UP2__[p2]); + score += this.ts_(this.UP3__[p3]); + score += this.ts_(this.BP1__[p1 + p2]); + score += this.ts_(this.BP2__[p2 + p3]); + score += this.ts_(this.UW1__[w1]); + score += this.ts_(this.UW2__[w2]); + score += this.ts_(this.UW3__[w3]); + score += this.ts_(this.UW4__[w4]); + score += this.ts_(this.UW5__[w5]); + score += this.ts_(this.UW6__[w6]); + score += this.ts_(this.BW1__[w2 + w3]); + score += this.ts_(this.BW2__[w3 + w4]); + score += this.ts_(this.BW3__[w4 + w5]); + score += this.ts_(this.TW1__[w1 + w2 + w3]); + score += this.ts_(this.TW2__[w2 + w3 + w4]); + score += this.ts_(this.TW3__[w3 + w4 + w5]); + score += this.ts_(this.TW4__[w4 + w5 + w6]); + score += this.ts_(this.UC1__[c1]); + score += this.ts_(this.UC2__[c2]); + score += this.ts_(this.UC3__[c3]); + score += this.ts_(this.UC4__[c4]); + score += this.ts_(this.UC5__[c5]); + score += this.ts_(this.UC6__[c6]); + score += this.ts_(this.BC1__[c2 + c3]); + score += this.ts_(this.BC2__[c3 + c4]); + score += this.ts_(this.BC3__[c4 + c5]); + score += this.ts_(this.TC1__[c1 + c2 + c3]); + score += this.ts_(this.TC2__[c2 + c3 + c4]); + score += this.ts_(this.TC3__[c3 + c4 + c5]); + score += this.ts_(this.TC4__[c4 + c5 + c6]); +// score += this.ts_(this.TC5__[c4 + c5 + c6]); + score += this.ts_(this.UQ1__[p1 + c1]); + score += this.ts_(this.UQ2__[p2 + c2]); + score += this.ts_(this.UQ3__[p3 + c3]); + score += this.ts_(this.BQ1__[p2 + c2 + c3]); + score += this.ts_(this.BQ2__[p2 + c3 + c4]); + score += this.ts_(this.BQ3__[p3 + c2 + c3]); + score += this.ts_(this.BQ4__[p3 + c3 + c4]); + score += this.ts_(this.TQ1__[p2 + c1 + c2 + c3]); + score += this.ts_(this.TQ2__[p2 + c2 + c3 + c4]); + score += this.ts_(this.TQ3__[p3 + c1 + c2 + c3]); + score += this.ts_(this.TQ4__[p3 + c2 + c3 + c4]); + var p = "O"; + if (score > 0) { + result.push(word); + word = ""; + p = "B"; + } + p1 = p2; + p2 = p3; + p3 = p; + word += seg[i]; + } + result.push(word); + + return result; +} +export { TinySegmenter }; diff --git a/manifest.json b/manifest.json index 7c1d7a0..051e5ec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,14 +1,12 @@ -{ - "display_name": "LittleWhiteBox", - "loading_order": 10, - "requires": [], - "optional": [], - "js": "index.js", - "css": "style.css", - "author": "biex", - "version": "2.4.0", - "homePage": "https://github.com/RT15548/LittleWhiteBox", - "generate_interceptor": "xiaobaixGenerateInterceptor" - -} - +{ + "display_name": "LittleWhiteBox", + "loading_order": 10, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "biex", + "version": "2.4.0", + "homePage": "https://github.com/RT15548/LittleWhiteBox", + "generate_interceptor": "xiaobaixGenerateInterceptor" +} \ No newline at end of file diff --git a/modules/fourth-wall/fourth-wall.html b/modules/fourth-wall/fourth-wall.html index 1178c9d..910cef9 100644 --- a/modules/fourth-wall/fourth-wall.html +++ b/modules/fourth-wall/fourth-wall.html @@ -561,7 +561,7 @@ html, body { 配置 ══════════════════════════════════════════════════════════════════════════════ */ -const TTS_WORKER_URL = 'https://hstts.velure.top'; +const TTS_WORKER_URL = 'https://hstts.velure.codes'; const VALID_EMOTIONS = ['happy', 'sad', 'angry', 'surprise', 'scare', 'hate']; const EMOTION_ICONS = { diff --git a/modules/fourth-wall/fw-voice.js b/modules/fourth-wall/fw-voice.js index 29b2bba..c1574c2 100644 --- a/modules/fourth-wall/fw-voice.js +++ b/modules/fourth-wall/fw-voice.js @@ -2,7 +2,7 @@ // 语音模块 - TTS 合成服务 // ════════════════════════════════════════════════════════════════════════════ -export const TTS_WORKER_URL = 'https://hstts.velure.top'; +export const TTS_WORKER_URL = 'https://hstts.velure.codes'; export const DEFAULT_VOICE = 'female_1'; export const DEFAULT_SPEED = 1.0; diff --git a/modules/story-outline/story-outline-prompt.js b/modules/story-outline/story-outline-prompt.js index 44c4d11..56e0810 100644 --- a/modules/story-outline/story-outline-prompt.js +++ b/modules/story-outline/story-outline-prompt.js @@ -199,13 +199,6 @@ const DEFAULT_JSON_TEMPLATES = { ] } } -}`, - worldNewsRefresh: `{ - "world": { - "news": [ - { "title": "新闻标题", "time": "时间(可选)", "content": "新闻内容" } - ] - } }`, localMapGen: `{ "review": { @@ -268,7 +261,7 @@ const DEFAULT_PROMPTS = { stranger: { u1: v => `你是TRPG数据整理助手。从剧情文本中提取{{user}}遇到的陌生人/NPC,整理为JSON数组。`, a1: () => `明白。请提供【世界观】和【剧情经历】,我将提取角色并以JSON数组输出。`, - u2: v => `### 上下文\n\n**1. 世界观:**\n${worldInfo}\n\n**2. {{user}}经历:**\n${history(v.historyCount)}${v.storyOutline ? `\n\n**剧情大纲:**\n${wrap('story_outline', v.storyOutline)}` : ''}${nameList(v.existingContacts, v.existingStrangers)}\n\n### 输出要求\n\n1. 返回一个合法 JSON 数组,使用标准 JSON 语法(键名和字符串都用半角双引号 ")\n2. 只提取有具体称呼的角色\n3. 每个角色只需 name / location / info 三个字段\n4. 文本内容中如需使用引号,请使用单引号或中文引号「」或"",不要使用半角双引号 "\n5. 无新角色返回 []\n\n\n模板:${JSON_TEMPLATES.stranger}`, + u2: v => `### 上下文\n\n**1. 世界观:**\n${worldInfo}\n\n**2. {{user}}经历:**\n${history(v.historyCount)}${v.storyOutline ? `\n\n**剧情大纲:**\n${wrap('story_outline', v.storyOutline)}` : ''}${nameList(v.existingContacts, v.existingStrangers)}\n\n### 输出要求\n\n1. 返回一个合法 JSON 数组,使用标准 JSON 语法(键名和字符串都用半角双引号 ")\n2. 只提取有具体称呼的角色\n3. 每个角色只需 name / location / info 三个字段\n4. 文本内容中如需使用引号,请使用单引号或中文引号「」或"",不要使用半角双引号 "\n5. 无新角色返回 []\n\n\n模板:${JSON_TEMPLATES.npc}`, a2: () => `了解,开始生成JSON:` }, worldGenStep1: { @@ -380,12 +373,6 @@ const DEFAULT_PROMPTS = { u2: v => `【世界观设定】:\n${worldInfo}\n\n【{{user}}历史】:\n${history(v.historyCount)}\n\n【当前世界状态JSON】(可能包含 meta/world/maps 等字段):\n${v.currentWorldData || '{}'}\n\n【JSON模板(辅助模式)】:\n${JSON_TEMPLATES.worldSimAssist}`, a2: () => `开始按 worldSimAssist 模板输出JSON:` }, - worldNewsRefresh: { - u1: v => `你是世界新闻编辑。基于世界观设定与{{user}}近期经历,为世界生成「最新资讯」。\n\n要求:\n1) 只输出 world.news(不要输出 maps/meta/其他字段)。\n2) news 至少 ${randomRange(2, 4)} 条;语气轻松、中性,夹带少量日常生活细节;可以包含与主剧情相关的跟进报道。\n3) 只输出符合模板的 JSON,禁止解释文字。\n\n- 使用标准 JSON 语法:所有键名与字符串都使用半角双引号\n- 文本内容如需使用引号,请使用单引号或中文引号「」/“”,不要使用半角双引号`, - a1: () => `明白。我将只更新 world.news,不改动世界其它字段。请提供当前世界数据。`, - u2: v => `【世界观设定】:\n${worldInfo}\n\n【{{user}}历史】:\n${history(v.historyCount)}\n\n【当前世界状态JSON】(可能包含 meta/world/maps 等字段):\n${v.currentWorldData || '{}'}\n\n【JSON模板】:\n${JSON_TEMPLATES.worldNewsRefresh}`, - a2: () => `OK, worldNewsRefresh JSON generate start:` - }, localMapGen: { u1: v => `你是TRPG局部场景生成器。你的任务是根据聊天历史,推断{{user}}当前或将要前往的位置(视经历的最后一条消息而定),并为该位置生成详细的局部地图/室内场景。 @@ -602,7 +589,6 @@ export const buildExtractStrangersMessages = v => build('stranger', v); export const buildWorldGenStep1Messages = v => build('worldGenStep1', v); export const buildWorldGenStep2Messages = v => build('worldGenStep2', v); export const buildWorldSimMessages = v => build(v?.mode === 'assist' ? 'worldSimAssist' : 'worldSim', v); -export const buildWorldNewsRefreshMessages = v => build('worldNewsRefresh', v); export const buildSceneSwitchMessages = v => build('sceneSwitch', v); export const buildLocalMapGenMessages = v => build('localMapGen', v); export const buildLocalMapRefreshMessages = v => build('localMapRefresh', v); diff --git a/modules/story-outline/story-outline.html b/modules/story-outline/story-outline.html index 417d681..51906fc 100644 --- a/modules/story-outline/story-outline.html +++ b/modules/story-outline/story-outline.html @@ -1,2885 +1,2877 @@ - - - - - - - 小白板 - - - - - - -
-
-
-
- - - -
-
- -
- - -
-
小白板预测试
- - - - -
- - -
- -
- - -
-
-

最新消息

- -
-
-
- -
-

当前状态

-
尚未生成世界数据...
-

行动指南

-
-
等待世界生成...
-
-
-
- - -
-
-
- - 大地图 - - -
-
- -
-
-
- -
100%
-
-
-
-
-
-
← 返回
-
-
-
-
-
- - -
-
-
-
陌路人
-
联络人
-
- - -
-
-
-
-
- - -
-
-
-
-
-
-
-
- - - -
-
-
-
- - - -
-
- - -
-
-
-
-
-
- 场景描述 - -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
← 返回
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + 小白板 + + + + + + +
+
+
+
+ + + +
+
+ +
+ + +
+
小白板预测试
+ + + + +
+ + +
+ +
+ + +
+
+

最新消息

+ +
+
+
+ +
+

当前状态

+
尚未生成世界数据...
+

行动指南

+
+
等待世界生成...
+
+
+
+ + +
+
+
+ + 大地图 + + +
+
+ +
+
+
+ +
100%
+
+
+
+
+
+
← 返回
+
+
+
+
+
+ + +
+
+
+
陌路人
+
联络人
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+ + +
+
+
+
+
+
+ 场景描述 + +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
← 返回
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/story-outline/story-outline.js b/modules/story-outline/story-outline.js index d4e394b..aa01a92 100644 --- a/modules/story-outline/story-outline.js +++ b/modules/story-outline/story-outline.js @@ -21,7 +21,7 @@ */ // ==================== 1. 导入与常量 ==================== -import { extension_settings, saveMetadataDebounced, writeExtensionField } from "../../../../../extensions.js"; +import { extension_settings, saveMetadataDebounced } from "../../../../../extensions.js"; import { chat_metadata, name1, processCommands, eventSource, event_types as st_event_types } from "../../../../../../script.js"; import { loadWorldInfo, saveWorldInfo, world_names, world_info } from "../../../../../world-info.js"; import { getContext } from "../../../../../st-context.js"; @@ -33,7 +33,7 @@ import { promptManager } from "../../../../../openai.js"; import { buildSmsMessages, buildSummaryMessages, buildSmsHistoryContent, buildExistingSummaryContent, buildNpcGenerationMessages, formatNpcToWorldbookContent, buildExtractStrangersMessages, - buildWorldGenStep1Messages, buildWorldGenStep2Messages, buildWorldSimMessages, buildWorldNewsRefreshMessages, buildSceneSwitchMessages, + buildWorldGenStep1Messages, buildWorldGenStep2Messages, buildWorldSimMessages, buildSceneSwitchMessages, buildInviteMessages, buildLocalMapGenMessages, buildLocalMapRefreshMessages, buildLocalSceneGenMessages, buildOverlayHtml, MOBILE_LAYOUT_STYLE, DESKTOP_LAYOUT_STYLE, getPromptConfigPayload, setPromptConfig } from "./story-outline-prompt.js"; @@ -48,86 +48,6 @@ const DEBUG_KEY = 'LittleWhiteBox_StoryOutline_Debug'; let overlayCreated = false, frameReady = false, pendingMsgs = [], presetCleanup = null, step1Cache = null; -// ==================== PromptConfig (global + character card) ==================== -// Global: server storage key `promptConfig` (old behavior) -// Character: character-card extension field (see scheduled-tasks implementation) - -const PROMPTS_MODULE_NAME = 'xiaobaix-story-outline-prompts'; - -let promptConfigGlobal = { jsonTemplates: {}, promptSources: {} }; -let promptConfigCharacter = { jsonTemplates: {}, promptSources: {} }; -let promptConfigCharacterId = null; - -function normalizePromptConfig(cfg) { - const src = (cfg && typeof cfg === 'object') ? cfg : {}; - const out = { - jsonTemplates: { ...(src.jsonTemplates || {}) }, - promptSources: {}, - }; - const ps = src.promptSources || src.prompts || {}; - Object.entries(ps).forEach(([k, v]) => { - if (!v || typeof v !== 'object' || Array.isArray(v)) return; - out.promptSources[k] = { ...v }; - }); - return out; -} - -function mergePromptConfig(globalCfg, charCfg) { - const g = normalizePromptConfig(globalCfg); - const c = normalizePromptConfig(charCfg); - - const mergedPromptSources = { ...(g.promptSources || {}) }; - Object.entries(c.promptSources || {}).forEach(([key, parts]) => { - const base = mergedPromptSources[key]; - mergedPromptSources[key] = (base && typeof base === 'object' && !Array.isArray(base)) - ? { ...base, ...(parts || {}) } - : { ...(parts || {}) }; - }); - - return { - jsonTemplates: { ...(g.jsonTemplates || {}), ...(c.jsonTemplates || {}) }, - promptSources: mergedPromptSources, - }; -} - -function getCharacterPromptConfig() { - const ctx = getContext?.(); - const charId = ctx?.characterId ?? null; - const char = (charId != null) ? ctx?.characters?.[charId] : null; - const cfg = char?.data?.extensions?.[PROMPTS_MODULE_NAME]?.promptConfig || null; - return normalizePromptConfig(cfg); -} - -async function saveCharacterPromptConfig(cfg) { - const ctx = getContext?.(); - const charId = ctx?.characterId ?? null; - if (charId == null) return; - - const payload = { promptConfig: normalizePromptConfig(cfg) }; - await writeExtensionField(Number(charId), PROMPTS_MODULE_NAME, payload); - - // Keep in-memory character extension in sync (same pattern as scheduled-tasks). - try { - const char = ctx?.characters?.[charId]; - if (char) { - if (!char.data) char.data = {}; - if (!char.data.extensions) char.data.extensions = {}; - char.data.extensions[PROMPTS_MODULE_NAME] = payload; - } - } catch { } -} - -function getPromptConfigPayloadWithStores() { - const base = getPromptConfigPayload?.() || {}; - return { - ...base, - stores: { - global: normalizePromptConfig(promptConfigGlobal), - character: normalizePromptConfig(promptConfigCharacter), - }, - }; -} - // ==================== 2. 通用工具 ==================== /** 移动端检测 */ @@ -691,40 +611,17 @@ function postFrame(payload) { const flushPending = () => { if (!frameReady) return; const f = document.getElementById("xiaobaix-story-outline-iframe"); pendingMsgs.forEach(p => { if (f) postToIframe(f, p, "LittleWhiteBox"); }); pendingMsgs = []; }; -async function syncPromptConfigForCurrentCharacter() { - const ctx = getContext?.(); - const charId = ctx?.characterId ?? null; - - if (promptConfigCharacterId !== charId) { - promptConfigCharacterId = charId; - promptConfigCharacter = getCharacterPromptConfig(); - } - - try { - const cfg = await StoryOutlineStorage?.get?.('promptConfig', null); - promptConfigGlobal = normalizePromptConfig(cfg); - } catch { - promptConfigGlobal = { jsonTemplates: {}, promptSources: {} }; - } - - const merged = mergePromptConfig(promptConfigGlobal, promptConfigCharacter); - setPromptConfig?.(merged, false); - postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayloadWithStores() }); -} - /** 发送设置到iframe */ function sendSettings() { const store = getOutlineStore(), { name: charName, desc: charDesc } = getCharInfo(); - syncPromptConfigForCurrentCharacter().catch(() => { }); postFrame({ type: "LOAD_SETTINGS", globalSettings: getGlobalSettings(), commSettings: getCommSettings(), stage: store?.stage ?? 0, deviationScore: store?.deviationScore ?? 0, simulationTarget: store?.simulationTarget ?? 5, playerLocation: store?.playerLocation ?? '家', - dataChecked: store?.dataChecked || {}, outlineData: store?.outlineData || {}, promptConfig: getPromptConfigPayloadWithStores(), + dataChecked: store?.dataChecked || {}, outlineData: store?.outlineData || {}, promptConfig: getPromptConfigPayload?.(), characterCardName: charName, characterCardDescription: charDesc, characterContactSmsHistory: getCharSmsHistory() }); - } const loadAndSend = () => { const s = getOutlineStore(); if (s?.mapData) postFrame({ type: "LOAD_MAP_DATA", mapData: s.mapData }); sendSettings(); }; @@ -814,7 +711,6 @@ const V = { wg1: d => !!d && typeof d === 'object', // 只要是对象就行,后续会 normalize wg2: d => !!((d?.world && (d?.maps || d?.world?.maps)?.outdoor) || (d?.outdoor && d?.inside)), wga: d => !!((d?.world && d?.maps?.outdoor) || d?.outdoor), ws: d => !!d, w: o => !!o && typeof o === 'object', - wn: d => Array.isArray(d?.world?.news), lm: o => !!o?.inside?.name && !!o?.inside?.description }; @@ -1198,34 +1094,6 @@ async function handleSimWorld({ requestId, currentData, isAuto }) { } catch (e) { replyErr('SIMULATE_WORLD_RESULT', requestId, `推演失败: ${e.message}`); } } -async function handleRefreshWorldNews({ requestId }) { - try { - const store = getOutlineStore(); - const od = store?.outlineData; - if (!od) return replyErr('REFRESH_WORLD_NEWS_RESULT', requestId, '未找到世界数据,请先生成世界'); - - // Store may persist maps either under `maps` or as `outdoor/indoor` (iframe SAVE_ALL_DATA format). - const maps = od?.maps || { outdoor: od?.outdoor || null, indoor: od?.indoor || null }; - const snapshot = { - meta: od?.meta || {}, - world: od?.world || {}, - maps, - ...(od?.timeline ? { timeline: od.timeline } : {}), - }; - - const msgs = buildWorldNewsRefreshMessages(getCommonPromptVars({ - currentWorldData: JSON.stringify(snapshot, null, 2), - })); - - const data = await callLLMJson({ messages: msgs, validate: V.wn }); - if (!Array.isArray(data?.world?.news)) return replyErr('REFRESH_WORLD_NEWS_RESULT', requestId, '世界新闻刷新失败:无法解析 JSON 数据'); - - reply('REFRESH_WORLD_NEWS_RESULT', requestId, { success: true, news: data.world.news }); - } catch (e) { - replyErr('REFRESH_WORLD_NEWS_RESULT', requestId, `世界新闻刷新失败: ${e.message}`); - } -} - function handleSaveSettings(d) { if (d.globalSettings) saveGlobalSettings(d.globalSettings); if (d.commSettings) saveCommSettings(d.commSettings); @@ -1247,56 +1115,39 @@ function handleSaveSettings(d) { } async function handleSavePrompts(d) { - const scope = d?.scope === 'character' ? 'character' : 'global'; - - // Back-compat: full payload (old iframe) -> treat as global save (old server storage behavior). + // Back-compat: full payload (old iframe) if (d?.promptConfig) { - promptConfigGlobal = normalizePromptConfig(d.promptConfig); - try { await StoryOutlineStorage?.set?.('promptConfig', promptConfigGlobal); } catch { } - - // Re-read current character config (if any) and apply merged. - promptConfigCharacter = getCharacterPromptConfig(); - const merged = mergePromptConfig(promptConfigGlobal, promptConfigCharacter); - setPromptConfig?.(merged, false); - postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayloadWithStores() }); + const payload = setPromptConfig?.(d.promptConfig, false) || d.promptConfig; + try { await StoryOutlineStorage?.set?.('promptConfig', payload); } catch { } + postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() }); return; } + // New: incremental update by key const key = d?.key; if (!key) return; - // Always merge against the latest global config from server storage. - try { promptConfigGlobal = normalizePromptConfig(await StoryOutlineStorage?.get?.('promptConfig', null)); } catch { } - // Always merge against the current character-card config. - promptConfigCharacterId = getContext?.()?.characterId ?? null; - promptConfigCharacter = getCharacterPromptConfig(); + let current = null; + try { current = await StoryOutlineStorage?.get?.('promptConfig', null); } catch { } + const next = (current && typeof current === 'object') ? { + jsonTemplates: { ...(current.jsonTemplates || {}) }, + promptSources: { ...(current.promptSources || {}) }, + } : { jsonTemplates: {}, promptSources: {} }; - const applyDelta = (cfg) => { - const next = normalizePromptConfig(cfg); - if (d?.reset) { - delete next.promptSources[key]; - delete next.jsonTemplates[key]; - return next; - } + if (d?.reset) { + delete next.promptSources[key]; + delete next.jsonTemplates[key]; + } else { if (d?.prompt && typeof d.prompt === 'object') next.promptSources[key] = d.prompt; if ('jsonTemplate' in (d || {})) { if (d.jsonTemplate == null) delete next.jsonTemplates[key]; else next.jsonTemplates[key] = String(d.jsonTemplate ?? ''); } - return next; - }; - - if (scope === 'character') { - promptConfigCharacter = applyDelta(promptConfigCharacter); - await saveCharacterPromptConfig(promptConfigCharacter); - } else { - promptConfigGlobal = applyDelta(promptConfigGlobal); - try { await StoryOutlineStorage?.set?.('promptConfig', promptConfigGlobal); } catch { } } - const merged = mergePromptConfig(promptConfigGlobal, promptConfigCharacter); - setPromptConfig?.(merged, false); - postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayloadWithStores() }); + const payload = setPromptConfig?.(next, false) || next; + try { await StoryOutlineStorage?.set?.('promptConfig', payload); } catch { } + postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() }); } function handleSaveContacts(d) { @@ -1357,7 +1208,6 @@ const handlers = { GENERATE_WORLD: handleGenWorld, RETRY_WORLD_GEN_STEP2: handleRetryStep2, SIMULATE_WORLD: handleSimWorld, - REFRESH_WORLD_NEWS: handleRefreshWorldNews, GENERATE_LOCAL_MAP: handleGenLocalMap, REFRESH_LOCAL_MAP: handleRefreshLocalMap, GENERATE_LOCAL_SCENE: handleGenLocalScene @@ -1520,7 +1370,10 @@ document.addEventListener('xiaobaixEnabledChanged', e => { async function initPromptConfigFromServer() { try { - await syncPromptConfigForCurrentCharacter(); + const cfg = await StoryOutlineStorage?.get?.('promptConfig', null); + if (!cfg) return; + setPromptConfig?.(cfg, false); + postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() }); } catch { } } diff --git a/modules/story-summary/data/config.js b/modules/story-summary/data/config.js new file mode 100644 index 0000000..77d6832 --- /dev/null +++ b/modules/story-summary/data/config.js @@ -0,0 +1,141 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// Story Summary - Config (v2 简化版) +// ═══════════════════════════════════════════════════════════════════════════ + +import { extension_settings } from "../../../../../../extensions.js"; +import { EXT_ID } from "../../../core/constants.js"; +import { xbLog } from "../../../core/debug-core.js"; +import { CommonSettingStorage } from "../../../core/server-storage.js"; + +const MODULE_ID = 'summaryConfig'; +const SUMMARY_CONFIG_KEY = 'storySummaryPanelConfig'; + +export function getSettings() { + const ext = extension_settings[EXT_ID] ||= {}; + ext.storySummary ||= { enabled: true }; + return ext; +} + +const DEFAULT_FILTER_RULES = [ + { start: '', end: '' }, + { start: '', end: '' }, +]; + +export function getSummaryPanelConfig() { + const defaults = { + api: { provider: 'st', url: '', key: '', model: '', modelCache: [] }, + gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null }, + trigger: { + enabled: false, + interval: 20, + timing: 'before_user', + role: 'system', + useStream: true, + maxPerRun: 100, + wrapperHead: '', + wrapperTail: '', + forceInsertAtEnd: false, + }, + vector: null, + }; + + try { + const raw = localStorage.getItem('summary_panel_config'); + if (!raw) return defaults; + const parsed = JSON.parse(raw); + + const result = { + api: { ...defaults.api, ...(parsed.api || {}) }, + gen: { ...defaults.gen, ...(parsed.gen || {}) }, + trigger: { ...defaults.trigger, ...(parsed.trigger || {}) }, + }; + + if (result.trigger.timing === 'manual') result.trigger.enabled = false; + if (result.trigger.useStream === undefined) result.trigger.useStream = true; + + return result; + } catch { + return defaults; + } +} + +export function saveSummaryPanelConfig(config) { + try { + localStorage.setItem('summary_panel_config', JSON.stringify(config)); + CommonSettingStorage.set(SUMMARY_CONFIG_KEY, config); + } catch (e) { + xbLog.error(MODULE_ID, '保存面板配置失败', e); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 向量配置(简化版 - 只需要 key) +// ═══════════════════════════════════════════════════════════════════════════ + +export function getVectorConfig() { + try { + const raw = localStorage.getItem('summary_panel_config'); + if (!raw) return null; + const parsed = JSON.parse(raw); + const cfg = parsed.vector || null; + + if (cfg && !cfg.textFilterRules) { + cfg.textFilterRules = [...DEFAULT_FILTER_RULES]; + } + + // 简化:统一使用硅基 + if (cfg) { + cfg.engine = 'online'; + cfg.online = cfg.online || {}; + cfg.online.provider = 'siliconflow'; + cfg.online.model = 'BAAI/bge-m3'; + } + + return cfg; + } catch { + return null; + } +} + +export function getTextFilterRules() { + const cfg = getVectorConfig(); + return cfg?.textFilterRules || DEFAULT_FILTER_RULES; +} + +export function saveVectorConfig(vectorCfg) { + try { + const raw = localStorage.getItem('summary_panel_config') || '{}'; + const parsed = JSON.parse(raw); + + // 简化配置 + parsed.vector = { + enabled: vectorCfg?.enabled || false, + engine: 'online', + online: { + provider: 'siliconflow', + key: vectorCfg?.online?.key || '', + model: 'BAAI/bge-m3', + }, + textFilterRules: vectorCfg?.textFilterRules || DEFAULT_FILTER_RULES, + }; + + localStorage.setItem('summary_panel_config', JSON.stringify(parsed)); + CommonSettingStorage.set(SUMMARY_CONFIG_KEY, parsed); + } catch (e) { + xbLog.error(MODULE_ID, '保存向量配置失败', e); + } +} + +export async function loadConfigFromServer() { + try { + const savedConfig = await CommonSettingStorage.get(SUMMARY_CONFIG_KEY, null); + if (savedConfig) { + localStorage.setItem('summary_panel_config', JSON.stringify(savedConfig)); + xbLog.info(MODULE_ID, '已从服务器加载面板配置'); + return savedConfig; + } + } catch (e) { + xbLog.warn(MODULE_ID, '加载面板配置失败', e); + } + return null; +} diff --git a/modules/story-summary/data/db.js b/modules/story-summary/data/db.js new file mode 100644 index 0000000..540f110 --- /dev/null +++ b/modules/story-summary/data/db.js @@ -0,0 +1,26 @@ +// Memory Database (Dexie schema) + +import Dexie from '../../../libs/dexie.mjs'; + +const DB_NAME = 'LittleWhiteBox_Memory'; +const DB_VERSION = 3; // 升级版本 + +// Chunk parameters +export const CHUNK_MAX_TOKENS = 200; + +const db = new Dexie(DB_NAME); + +db.version(DB_VERSION).stores({ + meta: 'chatId', + chunks: '[chatId+chunkId], chatId, [chatId+floor]', + chunkVectors: '[chatId+chunkId], chatId', + eventVectors: '[chatId+eventId], chatId', + stateVectors: '[chatId+atomId], chatId, [chatId+floor]', // L0 向量表 +}); + +export { db }; +export const metaTable = db.meta; +export const chunksTable = db.chunks; +export const chunkVectorsTable = db.chunkVectors; +export const eventVectorsTable = db.eventVectors; +export const stateVectorsTable = db.stateVectors; diff --git a/modules/story-summary/data/store.js b/modules/story-summary/data/store.js new file mode 100644 index 0000000..0429d49 --- /dev/null +++ b/modules/story-summary/data/store.js @@ -0,0 +1,442 @@ +// Story Summary - Store +// L2 (events/characters/arcs) + L3 (facts) 统一存储 + +import { getContext, saveMetadataDebounced } from "../../../../../../extensions.js"; +import { chat_metadata } from "../../../../../../../script.js"; +import { EXT_ID } from "../../../core/constants.js"; +import { xbLog } from "../../../core/debug-core.js"; +import { clearEventVectors, deleteEventVectorsByIds } from "../vector/storage/chunk-store.js"; + +const MODULE_ID = 'summaryStore'; +const FACTS_LIMIT_PER_SUBJECT = 10; + +// ═══════════════════════════════════════════════════════════════════════════ +// 基础存取 +// ═══════════════════════════════════════════════════════════════════════════ + +export function getSummaryStore() { + const { chatId } = getContext(); + if (!chatId) return null; + chat_metadata.extensions ||= {}; + chat_metadata.extensions[EXT_ID] ||= {}; + chat_metadata.extensions[EXT_ID].storySummary ||= {}; + + const store = chat_metadata.extensions[EXT_ID].storySummary; + + // ★ 自动迁移旧数据 + if (store.json && !store.json.facts) { + const hasOldData = store.json.world?.length || store.json.characters?.relationships?.length; + if (hasOldData) { + store.json.facts = migrateToFacts(store.json); + // 删除旧字段 + delete store.json.world; + if (store.json.characters) { + delete store.json.characters.relationships; + } + store.updatedAt = Date.now(); + saveSummaryStore(); + xbLog.info(MODULE_ID, `自动迁移完成: ${store.json.facts.length} 条 facts`); + } + } + + return store; +} + +export function saveSummaryStore() { + saveMetadataDebounced?.(); +} + +export function getKeepVisibleCount() { + const store = getSummaryStore(); + return store?.keepVisibleCount ?? 3; +} + +export function calcHideRange(boundary) { + if (boundary == null || boundary < 0) return null; + + const keepCount = getKeepVisibleCount(); + const hideEnd = boundary - keepCount; + if (hideEnd < 0) return null; + return { start: 0, end: hideEnd }; +} + +export function addSummarySnapshot(store, endMesId) { + store.summaryHistory ||= []; + store.summaryHistory.push({ endMesId }); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Fact 工具函数 +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * 判断是否为关系类 fact + */ +export function isRelationFact(f) { + return /^对.+的/.test(f.p); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 从 facts 提取关系(供关系图 UI 使用) +// ═══════════════════════════════════════════════════════════════════════════ + +export function extractRelationshipsFromFacts(facts) { + return (facts || []) + .filter(f => !f.retracted && isRelationFact(f)) + .map(f => { + const match = f.p.match(/^对(.+)的/); + const to = match ? match[1] : ''; + if (!to) return null; + return { + from: f.s, + to, + label: f.o, + trend: f.trend || '陌生', + }; + }) + .filter(Boolean); +} + +/** + * 生成 fact 的唯一键(s + p) + */ +function factKey(f) { + return `${f.s}::${f.p}`; +} + +/** + * 生成下一个 fact ID + */ +function getNextFactId(existingFacts) { + let maxId = 0; + for (const f of existingFacts || []) { + const match = f.id?.match(/^f-(\d+)$/); + if (match) { + maxId = Math.max(maxId, parseInt(match[1], 10)); + } + } + return maxId + 1; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Facts 合并(KV 覆盖模型) +// ═══════════════════════════════════════════════════════════════════════════ + +export function mergeFacts(existingFacts, updates, floor) { + const map = new Map(); + + for (const f of existingFacts || []) { + if (!f.retracted) { + map.set(factKey(f), f); + } + } + + let nextId = getNextFactId(existingFacts); + + for (const u of updates || []) { + if (!u.s || !u.p) continue; + + const key = factKey(u); + + if (u.retracted === true) { + map.delete(key); + continue; + } + + if (!u.o || !String(u.o).trim()) continue; + + const existing = map.get(key); + const newFact = { + id: existing?.id || `f-${nextId++}`, + s: u.s.trim(), + p: u.p.trim(), + o: String(u.o).trim(), + since: floor, + _isState: existing?._isState ?? !!u.isState, + }; + + if (isRelationFact(newFact) && u.trend) { + newFact.trend = u.trend; + } + + if (existing?._addedAt != null) { + newFact._addedAt = existing._addedAt; + } else { + newFact._addedAt = floor; + } + + map.set(key, newFact); + } + + const factsBySubject = new Map(); + for (const f of map.values()) { + if (f._isState) continue; + const arr = factsBySubject.get(f.s) || []; + arr.push(f); + factsBySubject.set(f.s, arr); + } + + const toRemove = new Set(); + for (const arr of factsBySubject.values()) { + if (arr.length > FACTS_LIMIT_PER_SUBJECT) { + arr.sort((a, b) => (a._addedAt || 0) - (b._addedAt || 0)); + for (let i = 0; i < arr.length - FACTS_LIMIT_PER_SUBJECT; i++) { + toRemove.add(factKey(arr[i])); + } + } + } + + return Array.from(map.values()).filter(f => !toRemove.has(factKey(f))); +} + + +// ═══════════════════════════════════════════════════════════════════════════ +// 旧数据迁移 +// ═══════════════════════════════════════════════════════════════════════════ + +export function migrateToFacts(json) { + if (!json) return []; + + // 已有 facts 则跳过迁移 + if (json.facts?.length) return json.facts; + + const facts = []; + let nextId = 1; + + // 迁移 world(worldUpdate 的持久化结果) + for (const w of json.world || []) { + if (!w.category || !w.topic || !w.content) continue; + + let s, p; + + // 解析 topic 格式:status/knowledge/relation 用 "::" 分隔 + if (w.topic.includes('::')) { + [s, p] = w.topic.split('::').map(x => x.trim()); + } else { + // inventory/rule 类 + s = w.topic.trim(); + p = w.category; + } + + if (!s || !p) continue; + + facts.push({ + id: `f-${nextId++}`, + s, + p, + o: w.content.trim(), + since: w.floor ?? w._addedAt ?? 0, + _addedAt: w._addedAt ?? w.floor ?? 0, + }); + } + + // 迁移 relationships + for (const r of json.characters?.relationships || []) { + if (!r.from || !r.to) continue; + + facts.push({ + id: `f-${nextId++}`, + s: r.from, + p: `对${r.to}的看法`, + o: r.label || '未知', + trend: r.trend, + since: r._addedAt ?? 0, + _addedAt: r._addedAt ?? 0, + }); + } + + return facts; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 数据合并(L2 + L3) +// ═══════════════════════════════════════════════════════════════════════════ + +export function mergeNewData(oldJson, parsed, endMesId) { + const merged = structuredClone(oldJson || {}); + + // L2 初始化 + merged.keywords ||= []; + merged.events ||= []; + merged.characters ||= {}; + merged.characters.main ||= []; + merged.arcs ||= []; + + // L3 初始化(不再迁移,getSummaryStore 已处理) + merged.facts ||= []; + + // L2 数据合并 + if (parsed.keywords?.length) { + merged.keywords = parsed.keywords.map(k => ({ ...k, _addedAt: endMesId })); + } + + (parsed.events || []).forEach(e => { + e._addedAt = endMesId; + merged.events.push(e); + }); + + // newCharacters + const existingMain = new Set( + (merged.characters.main || []).map(m => typeof m === 'string' ? m : m.name) + ); + (parsed.newCharacters || []).forEach(name => { + if (!existingMain.has(name)) { + merged.characters.main.push({ name, _addedAt: endMesId }); + } + }); + + // arcUpdates + const arcMap = new Map((merged.arcs || []).map(a => [a.name, a])); + (parsed.arcUpdates || []).forEach(update => { + const existing = arcMap.get(update.name); + if (existing) { + existing.trajectory = update.trajectory; + existing.progress = update.progress; + if (update.newMoment) { + existing.moments = existing.moments || []; + existing.moments.push({ text: update.newMoment, _addedAt: endMesId }); + } + } else { + arcMap.set(update.name, { + name: update.name, + trajectory: update.trajectory, + progress: update.progress, + moments: update.newMoment ? [{ text: update.newMoment, _addedAt: endMesId }] : [], + _addedAt: endMesId, + }); + } + }); + merged.arcs = Array.from(arcMap.values()); + + // L3 factUpdates 合并 + merged.facts = mergeFacts(merged.facts, parsed.factUpdates || [], endMesId); + + return merged; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 回滚 +// ═══════════════════════════════════════════════════════════════════════════ + +export async function rollbackSummaryIfNeeded() { + const { chat, chatId } = getContext(); + const currentLength = Array.isArray(chat) ? chat.length : 0; + const store = getSummaryStore(); + + if (!store || store.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) { + return false; + } + + const lastSummarized = store.lastSummarizedMesId; + + if (currentLength <= lastSummarized) { + const deletedCount = lastSummarized + 1 - currentLength; + + if (deletedCount < 2) { + return false; + } + + xbLog.warn(MODULE_ID, `删除已总结楼层 ${deletedCount} 条,触发回滚`); + + const history = store.summaryHistory || []; + let targetEndMesId = -1; + + for (let i = history.length - 1; i >= 0; i--) { + if (history[i].endMesId < currentLength) { + targetEndMesId = history[i].endMesId; + break; + } + } + + await executeRollback(chatId, store, targetEndMesId, currentLength); + return true; + } + + return false; +} + +export async function executeRollback(chatId, store, targetEndMesId, currentLength) { + const oldEvents = store.json?.events || []; + + if (targetEndMesId < 0) { + store.lastSummarizedMesId = -1; + store.json = null; + store.summaryHistory = []; + store.hideSummarizedHistory = false; + + await clearEventVectors(chatId); + + } else { + const deletedEventIds = oldEvents + .filter(e => (e._addedAt ?? 0) > targetEndMesId) + .map(e => e.id); + + const json = store.json || {}; + + // L2 回滚 + json.events = (json.events || []).filter(e => (e._addedAt ?? 0) <= targetEndMesId); + json.keywords = (json.keywords || []).filter(k => (k._addedAt ?? 0) <= targetEndMesId); + json.arcs = (json.arcs || []).filter(a => (a._addedAt ?? 0) <= targetEndMesId); + json.arcs.forEach(a => { + a.moments = (a.moments || []).filter(m => + typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId + ); + }); + + if (json.characters) { + json.characters.main = (json.characters.main || []).filter(m => + typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId + ); + } + + // L3 facts 回滚 + json.facts = (json.facts || []).filter(f => (f._addedAt ?? 0) <= targetEndMesId); + + store.json = json; + store.lastSummarizedMesId = targetEndMesId; + store.summaryHistory = (store.summaryHistory || []).filter(h => h.endMesId <= targetEndMesId); + + if (deletedEventIds.length > 0) { + await deleteEventVectorsByIds(chatId, deletedEventIds); + xbLog.info(MODULE_ID, `回滚删除 ${deletedEventIds.length} 个事件向量`); + } + } + + store.updatedAt = Date.now(); + saveSummaryStore(); + + xbLog.info(MODULE_ID, `回滚完成,目标楼层: ${targetEndMesId}`); +} + +export async function clearSummaryData(chatId) { + const store = getSummaryStore(); + if (store) { + delete store.json; + store.lastSummarizedMesId = -1; + store.updatedAt = Date.now(); + saveSummaryStore(); + } + + if (chatId) { + await clearEventVectors(chatId); + } + + + xbLog.info(MODULE_ID, '总结数据已清空'); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// L3 数据读取(供 prompt.js / recall.js 使用) +// ═══════════════════════════════════════════════════════════════════════════ + +export function getFacts() { + const store = getSummaryStore(); + return (store?.json?.facts || []).filter(f => !f.retracted); +} + +export function getNewCharacters() { + const store = getSummaryStore(); + return (store?.json?.characters?.main || []).map(m => + typeof m === 'string' ? m : m.name + ); +} diff --git a/modules/story-summary/generate/generator.js b/modules/story-summary/generate/generator.js new file mode 100644 index 0000000..0a80e9b --- /dev/null +++ b/modules/story-summary/generate/generator.js @@ -0,0 +1,269 @@ +// Story Summary - Generator +// 调用 LLM 生成总结 + +import { getContext } from "../../../../../../extensions.js"; +import { xbLog } from "../../../core/debug-core.js"; +import { getSummaryStore, saveSummaryStore, addSummarySnapshot, mergeNewData, getFacts } from "../data/store.js"; +import { generateSummary, parseSummaryJson } from "./llm.js"; + +const MODULE_ID = 'summaryGenerator'; +const SUMMARY_SESSION_ID = 'xb9'; +const MAX_CAUSED_BY = 2; + +// ═══════════════════════════════════════════════════════════════════════════ +// factUpdates 清洗 +// ═══════════════════════════════════════════════════════════════════════════ + +function normalizeRelationPredicate(p) { + if (/^对.+的看法$/.test(p)) return p; + if (/^与.+的关系$/.test(p)) return p; + return null; +} + +function sanitizeFacts(parsed) { + if (!parsed) return; + + const updates = Array.isArray(parsed.factUpdates) ? parsed.factUpdates : []; + const ok = []; + + for (const item of updates) { + const s = String(item?.s || '').trim(); + const pRaw = String(item?.p || '').trim(); + + if (!s || !pRaw) continue; + + if (item.retracted === true) { + ok.push({ s, p: pRaw, retracted: true }); + continue; + } + + const o = String(item?.o || '').trim(); + if (!o) continue; + + const relP = normalizeRelationPredicate(pRaw); + const isRel = !!relP; + const fact = { + s, + p: isRel ? relP : pRaw, + o, + isState: !!item.isState, + }; + + if (isRel && item.trend) { + const validTrends = ['破裂', '厌恶', '反感', '陌生', '投缘', '亲密', '交融']; + if (validTrends.includes(item.trend)) { + fact.trend = item.trend; + } + } + + ok.push(fact); + } + + parsed.factUpdates = ok; +} + + +// ═══════════════════════════════════════════════════════════════════════════ +// causedBy 清洗(事件因果边) +// ═══════════════════════════════════════════════════════════════════════════ + +function sanitizeEventsCausality(parsed, existingEventIds) { + if (!parsed) return; + + const events = Array.isArray(parsed.events) ? parsed.events : []; + if (!events.length) return; + + const idRe = /^evt-\d+$/; + + const newIds = new Set( + events + .map(e => String(e?.id || '').trim()) + .filter(id => idRe.test(id)) + ); + + const allowed = new Set([...(existingEventIds || []), ...newIds]); + + for (const e of events) { + const selfId = String(e?.id || '').trim(); + if (!idRe.test(selfId)) { + e.causedBy = []; + continue; + } + + const raw = Array.isArray(e.causedBy) ? e.causedBy : []; + const out = []; + const seen = new Set(); + + for (const x of raw) { + const cid = String(x || '').trim(); + if (!idRe.test(cid)) continue; + if (cid === selfId) continue; + if (!allowed.has(cid)) continue; + if (seen.has(cid)) continue; + seen.add(cid); + out.push(cid); + if (out.length >= MAX_CAUSED_BY) break; + } + + e.causedBy = out; + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 辅助函数 +// ═══════════════════════════════════════════════════════════════════════════ + +export function formatExistingSummaryForAI(store) { + if (!store?.json) return "(空白,这是首次总结)"; + + const data = store.json; + const parts = []; + + if (data.events?.length) { + parts.push("【已记录事件】"); + data.events.forEach((ev, i) => parts.push(`${i + 1}. [${ev.timeLabel}] ${ev.title}:${ev.summary}`)); + } + + if (data.characters?.main?.length) { + const names = data.characters.main.map(m => typeof m === 'string' ? m : m.name); + parts.push(`\n【主要角色】${names.join("、")}`); + } + + if (data.arcs?.length) { + parts.push("【角色弧光】"); + data.arcs.forEach(a => parts.push(`- ${a.name}:${a.trajectory}(进度${Math.round(a.progress * 100)}%)`)); + } + + if (data.keywords?.length) { + parts.push(`\n【关键词】${data.keywords.map(k => k.text).join("、")}`); + } + + return parts.join("\n") || "(空白,这是首次总结)"; +} + +export function getNextEventId(store) { + const events = store?.json?.events || []; + if (!events.length) return 1; + + const maxId = Math.max(...events.map(e => { + const match = e.id?.match(/evt-(\d+)/); + return match ? parseInt(match[1]) : 0; + })); + + return maxId + 1; +} + +export function buildIncrementalSlice(targetMesId, lastSummarizedMesId, maxPerRun = 100) { + const { chat, name1, name2 } = getContext(); + + const start = Math.max(0, (lastSummarizedMesId ?? -1) + 1); + const rawEnd = Math.min(targetMesId, chat.length - 1); + const end = Math.min(rawEnd, start + maxPerRun - 1); + + if (start > end) return { text: "", count: 0, range: "", endMesId: -1 }; + + const userLabel = name1 || '用户'; + const charLabel = name2 || '角色'; + const slice = chat.slice(start, end + 1); + + const text = slice.map((m, i) => { + const speaker = m.name || (m.is_user ? userLabel : charLabel); + return `#${start + i + 1} 【${speaker}】\n${m.mes}`; + }).join('\n\n'); + + return { text, count: slice.length, range: `${start + 1}-${end + 1}楼`, endMesId: end }; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 主生成函数 +// ═══════════════════════════════════════════════════════════════════════════ + +export async function runSummaryGeneration(mesId, config, callbacks = {}) { + const { onStatus, onError, onComplete } = callbacks; + + const store = getSummaryStore(); + const lastSummarized = store?.lastSummarizedMesId ?? -1; + const maxPerRun = config.trigger?.maxPerRun || 100; + const slice = buildIncrementalSlice(mesId, lastSummarized, maxPerRun); + + if (slice.count === 0) { + onStatus?.("没有新的对话需要总结"); + return { success: true, noContent: true }; + } + + onStatus?.(`正在总结 ${slice.range}(${slice.count}楼新内容)...`); + + const existingSummary = formatExistingSummaryForAI(store); + const existingFacts = getFacts(); + const nextEventId = getNextEventId(store); + const existingEventCount = store?.json?.events?.length || 0; + const useStream = config.trigger?.useStream !== false; + + let raw; + try { + raw = await generateSummary({ + existingSummary, + existingFacts, + newHistoryText: slice.text, + historyRange: slice.range, + nextEventId, + existingEventCount, + llmApi: { + provider: config.api?.provider, + url: config.api?.url, + key: config.api?.key, + model: config.api?.model, + }, + genParams: config.gen || {}, + useStream, + timeout: 120000, + sessionId: SUMMARY_SESSION_ID, + }); + } catch (err) { + xbLog.error(MODULE_ID, '生成失败', err); + onError?.(err?.message || "生成失败"); + return { success: false, error: err }; + } + + if (!raw?.trim()) { + xbLog.error(MODULE_ID, 'AI返回为空'); + onError?.("AI返回为空"); + return { success: false, error: "empty" }; + } + + const parsed = parseSummaryJson(raw); + if (!parsed) { + xbLog.error(MODULE_ID, 'JSON解析失败'); + onError?.("AI未返回有效JSON"); + return { success: false, error: "parse" }; + } + + sanitizeFacts(parsed); + const existingEventIds = new Set((store?.json?.events || []).map(e => e?.id).filter(Boolean)); + sanitizeEventsCausality(parsed, existingEventIds); + + const merged = mergeNewData(store?.json || {}, parsed, slice.endMesId); + + store.lastSummarizedMesId = slice.endMesId; + store.json = merged; + store.updatedAt = Date.now(); + addSummarySnapshot(store, slice.endMesId); + saveSummaryStore(); + + xbLog.info(MODULE_ID, `总结完成,已更新至 ${slice.endMesId + 1} 楼`); + + if (parsed.factUpdates?.length) { + xbLog.info(MODULE_ID, `Facts 更新: ${parsed.factUpdates.length} 条`); + } + + const newEventIds = (parsed.events || []).map(e => e.id); + + onComplete?.({ + merged, + endMesId: slice.endMesId, + newEventIds, + factStats: { updated: parsed.factUpdates?.length || 0 }, + }); + + return { success: true, merged, endMesId: slice.endMesId, newEventIds }; +} diff --git a/modules/story-summary/generate/llm.js b/modules/story-summary/generate/llm.js new file mode 100644 index 0000000..28c7e49 --- /dev/null +++ b/modules/story-summary/generate/llm.js @@ -0,0 +1,438 @@ +// LLM Service + +const PROVIDER_MAP = { + openai: "openai", + google: "gemini", + gemini: "gemini", + claude: "claude", + anthropic: "claude", + deepseek: "deepseek", + cohere: "cohere", + custom: "custom", +}; + +const JSON_PREFILL = '下面重新生成完整JSON。'; + +const LLM_PROMPT_CONFIG = { + topSystem: `Story Analyst: This task involves narrative comprehension and structured incremental summarization, representing creative story analysis at the intersection of plot tracking and character development. As a story analyst, you will conduct systematic evaluation of provided dialogue content to generate structured incremental summary data. +[Read the settings for this task] + +Incremental_Summary_Requirements: + - Incremental_Only: 只提取新对话中的新增要素,绝不重复已有总结 + - Event_Granularity: 记录有叙事价值的事件,而非剧情梗概 + - Memory_Album_Style: 形成有细节、有温度、有记忆点的回忆册 + - Event_Classification: + type: + - 相遇: 人物/事物初次接触 + - 冲突: 对抗、矛盾激化 + - 揭示: 真相、秘密、身份 + - 抉择: 关键决定 + - 羁绊: 关系加深或破裂 + - 转变: 角色/局势改变 + - 收束: 问题解决、和解 + - 日常: 生活片段 + weight: + - 核心: 删掉故事就崩 + - 主线: 推动主要剧情 + - 转折: 改变某条线走向 + - 点睛: 有细节不影响主线 + - 氛围: 纯粹氛围片段 + - Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。 + - Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融) + - Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0) + - Fact_Tracking: 维护 SPO 三元组知识图谱。追踪生死、物品归属、位置、关系等硬性事实。采用 KV 覆盖模型(s+p 为键)。 + +--- +Story Analyst: +[Responsibility Definition] +\`\`\`yaml +analysis_task: + title: Incremental Story Summarization with Knowledge Graph + Story Analyst: + role: Antigravity + task: >- + To analyze provided dialogue content against existing summary state, + extract only NEW plot elements, character developments, relationship + changes, arc progressions, AND fact updates, outputting + structured JSON for incremental summary database updates. + assistant: + role: Summary Specialist + description: Incremental Story Summary & Knowledge Graph Analyst + behavior: >- + To compare new dialogue against existing summary, identify genuinely + new events and character interactions, classify events by narrative + type and weight, track character arc progression with percentage, + maintain facts as SPO triples with clear semantics, + and output structured JSON containing only incremental updates. + Must strictly avoid repeating any existing summary content. + user: + role: Content Provider + description: Supplies existing summary state and new dialogue + behavior: >- + To provide existing summary state (events, characters, arcs, facts) + and new dialogue content for incremental analysis. +interaction_mode: + type: incremental_analysis + output_format: structured_json + deduplication: strict_enforcement +execution_context: + summary_active: true + incremental_only: true + memory_album_style: true + fact_tracking: true +\`\`\` +--- +Summary Specialist: +`, + + assistantDoc: ` +Summary Specialist: +Acknowledged. Now reviewing the incremental summarization specifications: + +[Event Classification System] +├─ Types: 相遇|冲突|揭示|抉择|羁绊|转变|收束|日常 +├─ Weights: 核心|主线|转折|点睛|氛围 +└─ Each event needs: id, title, timeLabel, summary(含楼层), participants, type, weight + +[Relationship Trend Scale] +破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融 + +[Arc Progress Tracking] +├─ trajectory: 当前阶段描述(15字内) +├─ progress: 0.0 to 1.0 +└─ newMoment: 仅记录本次新增的关键时刻 + +[Fact Tracking - SPO / World Facts] +We maintain a small "world state" as SPO triples. +Each update is a JSON object: {s, p, o, isState, trend?, retracted?} + +Core rules: +1) Keyed by (s + p). If a new update has the same (s+p), it overwrites the previous value. +2) Only output facts that are NEW or CHANGED in the new dialogue. Do NOT repeat unchanged facts. +3) isState meaning: + - isState: true -> core constraints that must stay stable and should NEVER be auto-deleted + (identity, location, life/death, ownership, relationship status, binding rules) + - isState: false -> non-core facts / soft memories that may be pruned by capacity limits later +4) Relationship facts: + - Use predicate format: "对X的看法" (X is the target person) + - trend is required for relationship facts, one of: + 破裂 | 厌恶 | 反感 | 陌生 | 投缘 | 亲密 | 交融 +5) Retraction (deletion): + - To delete a fact, output: {s, p, retracted: true} +6) Predicate normalization: + - Reuse existing predicates whenever possible, avoid inventing synonyms. + +Ready to process incremental summary requests with strict deduplication.`, + + assistantAskSummary: ` +Summary Specialist: +Specifications internalized. Please provide the existing summary state so I can: +1. Index all recorded events to avoid duplication +2. Map current character list as baseline +3. Note existing arc progress levels +4. Identify established keywords +5. Review current facts (SPO triples baseline)`, + + assistantAskContent: ` +Summary Specialist: +Existing summary fully analyzed and indexed. I understand: +├─ Recorded events: Indexed for deduplication +├─ Character list: Baseline mapped +├─ Arc progress: Levels noted +├─ Keywords: Current state acknowledged +└─ Facts: SPO baseline loaded + +I will extract only genuinely NEW elements from the upcoming dialogue. +Please provide the new dialogue content requiring incremental analysis.`, + + metaProtocolStart: ` +Summary Specialist: +ACKNOWLEDGED. Beginning structured JSON generation: +`, + + userJsonFormat: ` +## Output Rule +Generate a single valid JSON object with INCREMENTAL updates only. + +## Mindful Approach +Before generating, observe the USER and analyze carefully: +- What is user's writing style and emotional expression? +- What NEW events occurred (not in existing summary)? +- What NEW characters appeared for the first time? +- What relationship CHANGES happened? +- What arc PROGRESS was made? +- What facts changed? (status/position/ownership/relationships) + +## factUpdates 规则 +- 目的: 纠错 & 世界一致性约束,只记录硬性事实 +- s+p 为键,相同键会覆盖旧值 +- isState: true=核心约束(位置/身份/生死/关系),false=有容量上限会被清理 +- 关系类: p="对X的看法",trend 必填(破裂|厌恶|反感|陌生|投缘|亲密|交融) +- 删除: {s, p, retracted: true},不需要 o 字段 +- 更新: {s, p, o, isState, trend?} +- 谓词规范化: 复用已有谓词,不要发明同义词 +- 只输出有变化的条目,确保少、硬、稳定 + +## Output Format +\`\`\`json +{ + "mindful_prelude": { + "user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?", + "dedup_analysis": "已有X个事件,本次识别Y个新事件", + "fact_changes": "识别到的事实变化概述" + }, + "keywords": [ + {"text": "综合历史+新内容的全剧情关键词(5-10个)", "weight": "核心|重要|一般"} + ], + "events": [ + { + "id": "evt-{nextEventId}起始,依次递增", + "title": "地点·事件标题", + "timeLabel": "时间线标签(如:开场、第二天晚上)", + "summary": "1-2句话描述,涵盖丰富信息素,末尾标注楼层(#X-Y)", + "participants": ["参与角色名,不要使用人称代词或别名,只用正式人名"], + "type": "相遇|冲突|揭示|抉择|羁绊|转变|收束|日常", + "weight": "核心|主线|转折|点睛|氛围", + "causedBy": ["evt-12", "evt-14"] + } + ], + "newCharacters": ["仅本次首次出现的角色名"], + "arcUpdates": [ + {"name": "角色名,不要使用人称代词或别名,只用正式人名", "trajectory": "当前阶段描述(15字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"} + ], + "factUpdates": [ + {"s": "主体", "p": "谓词", "o": "当前值", "isState": true, "trend": "仅关系类填"}, + {"s": "要删除的主体", "p": "要删除的谓词", "retracted": true} + ] +} +\`\`\` + +## CRITICAL NOTES +- events.id 从 evt-{nextEventId} 开始编号 +- 仅输出【增量】内容,已有事件绝不重复 +- keywords 是全局关键词,综合已有+新增 +- causedBy 仅在因果明确时填写,允许为[],0-2个 +- factUpdates 可为空数组 +- 合法JSON,字符串值内部避免英文双引号 +- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象 +`, + + assistantCheck: `Content review initiated... +[Compliance Check Results] +├─ Existing summary loaded: ✓ Fully indexed +├─ New dialogue received: ✓ Content parsed +├─ Deduplication engine: ✓ Active +├─ Event classification: ✓ Ready +├─ Fact tracking: ✓ Enabled +└─ Output format: ✓ JSON specification loaded + +[Material Verification] +├─ Existing events: Indexed ({existingEventCount} recorded) +├─ Character baseline: Mapped +├─ Arc progress baseline: Noted +├─ Facts baseline: Loaded +└─ Output specification: ✓ Defined in +All checks passed. Beginning incremental extraction... +{ + "mindful_prelude":`, + + userConfirm: `怎么截断了!重新完整生成,只输出JSON,不要任何其他内容 +`, + + assistantPrefill: JSON_PREFILL +}; + +// ═══════════════════════════════════════════════════════════════════════════ +// 工具函数 +// ═══════════════════════════════════════════════════════════════════════════ + +function b64UrlEncode(str) { + const utf8 = new TextEncoder().encode(String(str)); + let bin = ''; + utf8.forEach(b => bin += String.fromCharCode(b)); + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function getStreamingModule() { + const mod = window.xiaobaixStreamingGeneration; + return mod?.xbgenrawCommand ? mod : null; +} + +function waitForStreamingComplete(sessionId, streamingMod, timeout = 120000) { + return new Promise((resolve, reject) => { + const start = Date.now(); + const poll = () => { + const { isStreaming, text } = streamingMod.getStatus(sessionId); + if (!isStreaming) return resolve(text || ''); + if (Date.now() - start > timeout) return reject(new Error('生成超时')); + setTimeout(poll, 300); + }; + poll(); + }); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 提示词构建 +// ═══════════════════════════════════════════════════════════════════════════ + +function formatFactsForLLM(facts) { + if (!facts?.length) { + return { text: '(空白,尚无事实记录)', predicates: [] }; + } + + const predicates = [...new Set(facts.map(f => f.p).filter(Boolean))]; + + const lines = facts.map(f => { + if (f.trend) { + return `- ${f.s} | ${f.p} | ${f.o} [${f.trend}]`; + } + return `- ${f.s} | ${f.p} | ${f.o}`; + }); + + return { + text: lines.join('\n') || '(空白,尚无事实记录)', + predicates, + }; +} + +function buildSummaryMessages(existingSummary, existingFacts, newHistoryText, historyRange, nextEventId, existingEventCount) { + const { text: factsText, predicates } = formatFactsForLLM(existingFacts); + + const predicatesHint = predicates.length > 0 + ? `\n\n<\u5df2\u6709\u8c13\u8bcd\uff0c\u8bf7\u590d\u7528>\n${predicates.join('\u3001')}\n` + : ''; + + const jsonFormat = LLM_PROMPT_CONFIG.userJsonFormat + .replace(/\{nextEventId\}/g, String(nextEventId)); + + const checkContent = LLM_PROMPT_CONFIG.assistantCheck + .replace(/\{existingEventCount\}/g, String(existingEventCount)); + + const topMessages = [ + { role: 'system', content: LLM_PROMPT_CONFIG.topSystem }, + { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantDoc }, + { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskSummary }, + { role: 'user', content: `<\u5df2\u6709\u603b\u7ed3\u72b6\u6001>\n${existingSummary}\n\n\n<\u5f53\u524d\u4e8b\u5b9e\u56fe\u8c31>\n${factsText}\n${predicatesHint}` }, + { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskContent }, + { role: 'user', content: `<\u65b0\u5bf9\u8bdd\u5185\u5bb9>\uff08${historyRange}\uff09\n${newHistoryText}\n` } + ]; + + const bottomMessages = [ + { role: 'user', content: LLM_PROMPT_CONFIG.metaProtocolStart + '\n' + jsonFormat }, + { role: 'assistant', content: checkContent }, + { role: 'user', content: LLM_PROMPT_CONFIG.userConfirm } + ]; + + return { + top64: b64UrlEncode(JSON.stringify(topMessages)), + bottom64: b64UrlEncode(JSON.stringify(bottomMessages)), + assistantPrefill: LLM_PROMPT_CONFIG.assistantPrefill + }; +} + + +// ═══════════════════════════════════════════════════════════════════════════ +// JSON 解析 +// ═══════════════════════════════════════════════════════════════════════════ + +export function parseSummaryJson(raw) { + if (!raw) return null; + + let cleaned = String(raw).trim() + .replace(/^```(?:json)?\s*/i, "") + .replace(/\s*```$/i, "") + .trim(); + + try { + return JSON.parse(cleaned); + } catch { } + + const start = cleaned.indexOf('{'); + const end = cleaned.lastIndexOf('}'); + if (start !== -1 && end > start) { + let jsonStr = cleaned.slice(start, end + 1) + .replace(/,(\s*[}\]])/g, '$1'); + try { + return JSON.parse(jsonStr); + } catch { } + } + + return null; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 主生成函数 +// ═══════════════════════════════════════════════════════════════════════════ + +export async function generateSummary(options) { + const { + existingSummary, + existingFacts, + newHistoryText, + historyRange, + nextEventId, + existingEventCount = 0, + llmApi = {}, + genParams = {}, + useStream = true, + timeout = 120000, + sessionId = 'xb_summary' + } = options; + + if (!newHistoryText?.trim()) { + throw new Error('新对话内容为空'); + } + + const streamingMod = getStreamingModule(); + if (!streamingMod) { + throw new Error('生成模块未加载'); + } + + const promptData = buildSummaryMessages( + existingSummary, + existingFacts, + newHistoryText, + historyRange, + nextEventId, + existingEventCount + ); + + const args = { + as: 'user', + nonstream: useStream ? 'false' : 'true', + top64: promptData.top64, + bottom64: promptData.bottom64, + bottomassistant: promptData.assistantPrefill, + id: sessionId, + }; + + if (llmApi.provider && llmApi.provider !== 'st') { + const mappedApi = PROVIDER_MAP[String(llmApi.provider).toLowerCase()]; + if (mappedApi) { + args.api = mappedApi; + if (llmApi.url) args.apiurl = llmApi.url; + if (llmApi.key) args.apipassword = llmApi.key; + if (llmApi.model) args.model = llmApi.model; + } + } + + if (genParams.temperature != null) args.temperature = genParams.temperature; + if (genParams.top_p != null) args.top_p = genParams.top_p; + if (genParams.top_k != null) args.top_k = genParams.top_k; + if (genParams.presence_penalty != null) args.presence_penalty = genParams.presence_penalty; + if (genParams.frequency_penalty != null) args.frequency_penalty = genParams.frequency_penalty; + + let rawOutput; + if (useStream) { + const sid = await streamingMod.xbgenrawCommand(args, ''); + rawOutput = await waitForStreamingComplete(sid, streamingMod, timeout); + } else { + rawOutput = await streamingMod.xbgenrawCommand(args, ''); + } + + console.group('%c[Story-Summary] LLM输出', 'color: #7c3aed; font-weight: bold'); + console.log(rawOutput); + console.groupEnd(); + + return JSON_PREFILL + rawOutput; +} diff --git a/modules/story-summary/generate/prompt.js b/modules/story-summary/generate/prompt.js new file mode 100644 index 0000000..162d650 --- /dev/null +++ b/modules/story-summary/generate/prompt.js @@ -0,0 +1,1413 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// Story Summary - Prompt Injection (v7 - L0 scene-based display) +// +// 命名规范: +// - 存储层用 L0/L1/L2/L3(StateAtom/Chunk/Event/Fact) +// - 装配层用语义名称:constraint/event/evidence/arc +// +// 架构变更(v5 → v6): +// - 同楼层多个 L0 共享一对 L1(EvidenceGroup per-floor) +// - L0 展示文本直接使用 semantic 字段(v7: 场景摘要,纯自然语言) +// - 仅负责"构建注入文本",不负责写入 extension_prompts +// - 注入发生在 story-summary.js:GENERATION_STARTED 时写入 extension_prompts +// ═══════════════════════════════════════════════════════════════════════════ + +import { getContext } from "../../../../../../extensions.js"; +import { xbLog } from "../../../core/debug-core.js"; +import { getSummaryStore, getFacts, isRelationFact } from "../data/store.js"; +import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js"; +import { recallMemory } from "../vector/retrieval/recall.js"; +import { getMeta } from "../vector/storage/chunk-store.js"; +import { getEngineFingerprint } from "../vector/utils/embedder.js"; +import { buildTrustedCharacters } from "../vector/retrieval/entity-lexicon.js"; + +// Metrics +import { formatMetricsLog, detectIssues } from "../vector/retrieval/metrics.js"; + +const MODULE_ID = "summaryPrompt"; + +// ───────────────────────────────────────────────────────────────────────────── +// 召回失败提示节流 +// ───────────────────────────────────────────────────────────────────────────── + +let lastRecallFailAt = 0; +const RECALL_FAIL_COOLDOWN_MS = 10_000; + +function canNotifyRecallFail() { + const now = Date.now(); + if (now - lastRecallFailAt < RECALL_FAIL_COOLDOWN_MS) return false; + lastRecallFailAt = now; + return true; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 预算常量 +// ───────────────────────────────────────────────────────────────────────────── + +const SHARED_POOL_MAX = 10000; +const CONSTRAINT_MAX = 2000; +const ARCS_MAX = 1500; +const EVENT_BUDGET_MAX = 5000; +const RELATED_EVENT_MAX = 500; +const SUMMARIZED_EVIDENCE_MAX = 1500; +const UNSUMMARIZED_EVIDENCE_MAX = 2000; +const TOP_N_STAR = 5; + +// L0 显示文本:分号拼接 vs 多行模式的阈值 +const L0_JOINED_MAX_LENGTH = 120; +// 背景证据:无实体匹配时保留的最低相似度(与 recall.js CONFIG.EVENT_ENTITY_BYPASS_SIM 保持一致) + +// ───────────────────────────────────────────────────────────────────────────── +// 工具函数 +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 估算文本 token 数量 + * @param {string} text - 输入文本 + * @returns {number} token 估算值 + */ +function estimateTokens(text) { + if (!text) return 0; + const s = String(text); + const zh = (s.match(/[\u4e00-\u9fff]/g) || []).length; + return Math.ceil(zh + (s.length - zh) / 4); +} + +/** + * 带预算限制的行追加 + * @param {string[]} lines - 行数组 + * @param {string} text - 要追加的文本 + * @param {object} state - 预算状态 {used, max} + * @returns {boolean} 是否追加成功 + */ +function pushWithBudget(lines, text, state) { + const t = estimateTokens(text); + if (state.used + t > state.max) return false; + lines.push(text); + state.used += t; + return true; +} + +/** + * 解析事件摘要中的楼层范围 + * @param {string} summary - 事件摘要 + * @returns {{start: number, end: number}|null} 楼层范围 + */ +function parseFloorRange(summary) { + if (!summary) return null; + const match = String(summary).match(/\(#(\d+)(?:-(\d+))?\)/); + if (!match) return null; + const start = Math.max(0, parseInt(match[1], 10) - 1); + const end = Math.max(0, (match[2] ? parseInt(match[2], 10) : parseInt(match[1], 10)) - 1); + return { start, end }; +} + +/** + * 清理事件摘要(移除楼层标记) + * @param {string} summary - 事件摘要 + * @returns {string} 清理后的摘要 + */ +function cleanSummary(summary) { + return String(summary || "") + .replace(/\s*\(#\d+(?:-\d+)?\)\s*$/, "") + .trim(); +} + +/** + * 标准化字符串 + * @param {string} s - 输入字符串 + * @returns {string} 标准化后的字符串 + */ +function normalize(s) { + return String(s || '') + .normalize('NFKC') + .replace(/[\u200B-\u200D\uFEFF]/g, '') + .trim() + .toLowerCase(); +} + +/** + * 收集 L0 的实体集合(用于背景证据实体过滤) + * 使用 edges.s/edges.t。 + * @param {object} l0 + * @returns {Set} + */ +function collectL0Entities(l0) { + const atom = l0?.atom || {}; + const set = new Set(); + + const add = (v) => { + const n = normalize(v); + if (n) set.add(n); + }; + + for (const e of (atom.edges || [])) { + add(e?.s); + add(e?.t); + } + + return set; +} + +/** + * 背景证据是否保留(按焦点实体过滤) + * 规则: + * 1) 无焦点实体:保留 + * 2) similarity >= 0.70:保留(旁通) + * 3) edges 命中焦点实体:保留 + * 否则过滤。 + * @param {object} l0 + * @param {Set} focusSet + * @returns {boolean} + */ +function shouldKeepEvidenceL0(l0, focusSet) { + if (!focusSet?.size) return false; + + const entities = collectL0Entities(l0); + for (const f of focusSet) { + if (entities.has(f)) return true; + } + + // 兼容旧数据:semantic 文本包含焦点实体 + const textNorm = normalize(l0?.atom?.semantic || l0?.text || ''); + for (const f of focusSet) { + if (f && textNorm.includes(f)) return true; + } + return false; +} + +/** + * 获取事件排序键 + * @param {object} event - 事件对象 + * @returns {number} 排序键 + */ +function getEventSortKey(event) { + const r = parseFloorRange(event?.summary); + if (r) return r.start; + const m = String(event?.id || "").match(/evt-(\d+)/); + return m ? parseInt(m[1], 10) : Number.MAX_SAFE_INTEGER; +} + +/** + * 重新编号事件文本 + * @param {string} text - 原始文本 + * @param {number} newIndex - 新编号 + * @returns {string} 重新编号后的文本 + */ +function renumberEventText(text, newIndex) { + const s = String(text || ""); + return s.replace(/^(\s*)\d+(\.\s*(?:【)?)/, `$1${newIndex}$2`); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 系统前导与后缀 +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 构建系统前导文本 + * @returns {string} 前导文本 + */ +function buildSystemPreamble() { + return [ + "以上是还留在眼前的对话", + "以下是脑海里的记忆:", + "• [定了的事] 这些是不会变的", + "• [其他人的事] 别人的经历,当前角色可能不知晓", + "• 其余部分是过往经历的回忆碎片", + "", + "请内化这些记忆:", + ].join("\n"); +} + +/** + * 构建后缀文本 + * @returns {string} 后缀文本 + */ +function buildPostscript() { + return [ + "", + "这些记忆是真实的,请自然地记住它们。", + ].join("\n"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// [Constraints] L3 Facts 过滤与格式化 +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 获取已知角色集合 + * @param {object} store - 存储对象 + * @returns {Set} 角色名称集合(标准化后) + */ +function getKnownCharacters(store) { + const { name1, name2 } = getContext(); + const names = buildTrustedCharacters(store, { name1, name2 }) || new Set(); + // Keep name1 in known-character filtering domain to avoid behavior regression + // for L3 subject filtering (lexicon exclusion and filtering semantics are different concerns). + if (name1) names.add(normalize(name1)); + return names; +} + +/** + * 解析关系谓词中的目标 + * @param {string} predicate - 谓词 + * @returns {string|null} 目标名称 + */ +function parseRelationTarget(predicate) { + const match = String(predicate || '').match(/^对(.+)的/); + return match ? match[1] : null; +} + +/** + * 按相关性过滤 facts + * @param {object[]} facts - 所有 facts + * @param {string[]} focusCharacters - 焦点人物 + * @param {Set} knownCharacters - 已知角色 + * @returns {object[]} 过滤后的 facts + */ +function filterConstraintsByRelevance(facts, focusCharacters, knownCharacters) { + if (!facts?.length) return []; + + const focusSet = new Set((focusCharacters || []).map(normalize)); + + return facts.filter(f => { + if (f._isState === true) return true; + + if (isRelationFact(f)) { + const from = normalize(f.s); + const target = parseRelationTarget(f.p); + const to = target ? normalize(target) : ''; + + if (focusSet.has(from) || focusSet.has(to)) return true; + return false; + } + + const subjectNorm = normalize(f.s); + if (knownCharacters.has(subjectNorm)) { + return focusSet.has(subjectNorm); + } + + return true; + }); +} + +/** + * Build people dictionary for constraints display. + * Primary source: selected event participants; fallback: focus characters. + * + * @param {object|null} recallResult + * @param {string[]} focusCharacters + * @returns {Map} normalize(name) -> display name + */ +function buildConstraintPeopleDict(recallResult, focusCharacters = []) { + const dict = new Map(); + const add = (raw) => { + const display = String(raw || '').trim(); + const key = normalize(display); + if (!display || !key) return; + if (!dict.has(key)) dict.set(key, display); + }; + + const selectedEvents = recallResult?.events || []; + for (const item of selectedEvents) { + const participants = item?.event?.participants || []; + for (const p of participants) add(p); + } + + if (dict.size === 0) { + for (const f of (focusCharacters || [])) add(f); + } + + return dict; +} + +/** + * Group filtered constraints into people/world buckets. + * @param {object[]} facts + * @param {Map} peopleDict + * @returns {{ people: Map, world: object[] }} + */ +function groupConstraintsForDisplay(facts, peopleDict) { + const people = new Map(); + const world = []; + + for (const f of (facts || [])) { + const subjectNorm = normalize(f?.s); + const displayName = peopleDict.get(subjectNorm); + if (displayName) { + if (!people.has(displayName)) people.set(displayName, []); + people.get(displayName).push(f); + } else { + world.push(f); + } + } + + return { people, world }; +} + +function formatConstraintLine(f, includeSubject = false) { + const subject = String(f?.s || '').trim(); + const predicate = String(f?.p || '').trim(); + const object = String(f?.o || '').trim(); + const trendRaw = String(f?.trend || '').trim(); + const hasSince = f?.since !== undefined && f?.since !== null; + const since = hasSince ? ` (#${f.since + 1})` : ''; + const trend = isRelationFact(f) && trendRaw ? ` [${trendRaw}]` : ''; + if (includeSubject) { + return `- ${subject} ${predicate}: ${object}${trend}${since}`; + } + return `- ${predicate}: ${object}${trend}${since}`; +} + +/** + * Render grouped constraints into structured human-readable lines. + * @param {{ people: Map, world: object[] }} grouped + * @returns {string[]} + */ +function formatConstraintsStructured(grouped, order = 'desc') { + const lines = []; + const people = grouped?.people || new Map(); + const world = grouped?.world || []; + const sorter = order === 'asc' + ? ((a, b) => (a.since || 0) - (b.since || 0)) + : ((a, b) => (b.since || 0) - (a.since || 0)); + + if (people.size > 0) { + lines.push('people:'); + for (const [name, facts] of people.entries()) { + lines.push(` ${name}:`); + const sorted = [...facts].sort(sorter); + for (const f of sorted) { + lines.push(` ${formatConstraintLine(f, false)}`); + } + } + } + + if (world.length > 0) { + lines.push('world:'); + const sortedWorld = [...world].sort(sorter); + for (const f of sortedWorld) { + lines.push(` ${formatConstraintLine(f, true)}`); + } + } + + return lines; +} + +function tryConsumeConstraintLineBudget(line, budgetState) { + const cost = estimateTokens(line); + if (budgetState.used + cost > budgetState.max) return false; + budgetState.used += cost; + return true; +} + +function selectConstraintsByBudgetDesc(grouped, budgetState) { + const selectedPeople = new Map(); + const selectedWorld = []; + const people = grouped?.people || new Map(); + const world = grouped?.world || []; + + if (people.size > 0) { + if (!tryConsumeConstraintLineBudget('people:', budgetState)) { + return { people: selectedPeople, world: selectedWorld }; + } + for (const [name, facts] of people.entries()) { + const header = ` ${name}:`; + if (!tryConsumeConstraintLineBudget(header, budgetState)) { + return { people: selectedPeople, world: selectedWorld }; + } + const picked = []; + const sorted = [...facts].sort((a, b) => (b.since || 0) - (a.since || 0)); + for (const f of sorted) { + const line = ` ${formatConstraintLine(f, false)}`; + if (!tryConsumeConstraintLineBudget(line, budgetState)) { + return { people: selectedPeople, world: selectedWorld }; + } + picked.push(f); + } + selectedPeople.set(name, picked); + } + } + + if (world.length > 0) { + if (!tryConsumeConstraintLineBudget('world:', budgetState)) { + return { people: selectedPeople, world: selectedWorld }; + } + const sortedWorld = [...world].sort((a, b) => (b.since || 0) - (a.since || 0)); + for (const f of sortedWorld) { + const line = ` ${formatConstraintLine(f, true)}`; + if (!tryConsumeConstraintLineBudget(line, budgetState)) { + return { people: selectedPeople, world: selectedWorld }; + } + selectedWorld.push(f); + } + } + + return { people: selectedPeople, world: selectedWorld }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 格式化函数 +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 格式化弧光行 + * @param {object} arc - 弧光对象 + * @returns {string} 格式化后的行 + */ +function formatArcLine(arc) { + const moments = (arc.moments || []) + .map(m => (typeof m === "string" ? m : m.text)) + .filter(Boolean); + + if (moments.length) { + return `- ${arc.name}:${moments.join(" → ")}`; + } + return `- ${arc.name}:${arc.trajectory}`; +} + +/** + * 从 L0 获取展示文本 + * + * v7: L0 的 semantic 字段已是纯自然语言场景摘要(60-100字),直接使用。 + * + * @param {object} l0 - L0 对象 + * @returns {string} 场景描述文本 + */ +function buildL0DisplayText(l0) { + const atom = l0.atom || {}; + return String(atom.semantic || l0.text || '').trim() || '(未知锚点)'; +} + +/** + * 格式化 L1 chunk 行 + * @param {object} chunk - L1 chunk 对象 + * @param {boolean} isContext - 是否为上下文(USER 侧) + * @returns {string} 格式化后的行 + */ +function formatL1Line(chunk, isContext) { + const { name1, name2 } = getContext(); + const speaker = chunk.isUser ? (name1 || "用户") : (chunk.speaker || name2 || "角色"); + const text = String(chunk.text || "").trim(); + const symbol = isContext ? "┌" : "›"; + return ` ${symbol} #${chunk.floor + 1} [${speaker}] ${text}`; +} + +/** + * 格式化因果事件行 + * @param {object} causalItem - 因果事件项 + * @returns {string} 格式化后的行 + */ +function formatCausalEventLine(causalItem) { + const ev = causalItem?.event || {}; + const depth = Math.max(1, Math.min(9, causalItem?._causalDepth || 1)); + const indent = " │" + " ".repeat(depth - 1); + const prefix = `${indent}├─ 前因`; + + const time = ev.timeLabel ? `【${ev.timeLabel}】` : ""; + const people = (ev.participants || []).join(" / "); + const summary = cleanSummary(ev.summary); + + const r = parseFloorRange(ev.summary); + const floorHint = r ? `(#${r.start + 1}${r.end !== r.start ? `-${r.end + 1}` : ""})` : ""; + + const lines = []; + lines.push(`${prefix}${time}${people ? ` ${people}` : ""}`); + const body = `${summary}${floorHint ? ` ${floorHint}` : ""}`.trim(); + lines.push(`${indent} ${body}`); + + return lines.join("\n"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// L0 按楼层分组 +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 将 L0 列表按楼层分组 + * @param {object[]} l0List - L0 对象列表 + * @returns {Map} floor → L0 数组 + */ +function groupL0ByFloor(l0List) { + const map = new Map(); + for (const l0 of l0List) { + const floor = l0.floor; + if (!map.has(floor)) { + map.set(floor, []); + } + map.get(floor).push(l0); + } + return map; +} + +// ───────────────────────────────────────────────────────────────────────────── +// EvidenceGroup(per-floor:N个L0 + 共享一对L1) +// ───────────────────────────────────────────────────────────────────────────── + +/** + * @typedef {object} EvidenceGroup + * @property {number} floor - 楼层号 + * @property {object[]} l0Atoms - 该楼层所有被选中的 L0 + * @property {object|null} userL1 - USER 侧 top-1 L1 chunk(仅一份) + * @property {object|null} aiL1 - AI 侧 top-1 L1 chunk(仅一份) + * @property {number} totalTokens - 整组 token 估算 + */ + +/** + * 为一个楼层构建证据组 + * + * 同楼层多个 L0 共享一对 L1,避免 L1 重复输出。 + * + * @param {number} floor - 楼层号 + * @param {object[]} l0AtomsForFloor - 该楼层所有被选中的 L0 + * @param {Map} l1ByFloor - 楼层→L1配对映射 + * @returns {EvidenceGroup} + */ +function buildEvidenceGroup(floor, l0AtomsForFloor, l1ByFloor) { + const pair = l1ByFloor.get(floor); + const userL1 = pair?.userTop1 || null; + const aiL1 = pair?.aiTop1 || null; + + // 计算整组 token 开销 + let totalTokens = 0; + + // 所有 L0 的显示文本 + for (const l0 of l0AtomsForFloor) { + totalTokens += estimateTokens(buildL0DisplayText(l0)); + } + // 固定开销:楼层前缀、📌 标记、分号等 + totalTokens += 10; + + // L1 仅算一次 + if (userL1) totalTokens += estimateTokens(formatL1Line(userL1, true)); + if (aiL1) totalTokens += estimateTokens(formatL1Line(aiL1, false)); + + return { floor, l0Atoms: l0AtomsForFloor, userL1, aiL1, totalTokens }; +} + +/** + * 格式化一个证据组为文本行数组 + * + * 短行模式(拼接后 ≤ 120 字): + * › #500 [📌] 小林整理会议记录;小周补充行动项;两人确认下周安排 + * ┌ #499 [小周] ... + * › #500 [角色] ... + * + * 长行模式(拼接后 > 120 字): + * › #500 [📌] 小林在图书馆归档旧资料 + * │ 小周核对目录并修正编号 + * │ 两人讨论借阅规则并更新说明 + * ┌ #499 [小周] ... + * › #500 [角色] ... + * + * @param {EvidenceGroup} group - 证据组 + * @returns {string[]} 文本行数组 + */ +function formatEvidenceGroup(group) { + const displayTexts = group.l0Atoms.map(l0 => buildL0DisplayText(l0)); + + const lines = []; + + // L0 部分 + const joined = displayTexts.join(';'); + + if (joined.length <= L0_JOINED_MAX_LENGTH) { + // 短行:分号拼接为一行 + lines.push(` › #${group.floor + 1} [📌] ${joined}`); + } else { + // 长行:每个 L0 独占一行,首行带楼层号 + lines.push(` › #${group.floor + 1} [📌] ${displayTexts[0]}`); + for (let i = 1; i < displayTexts.length; i++) { + lines.push(` │ ${displayTexts[i]}`); + } + } + + // L1 证据(仅一次) + if (group.userL1) { + lines.push(formatL1Line(group.userL1, true)); + } + if (group.aiL1) { + lines.push(formatL1Line(group.aiL1, false)); + } + + return lines; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 事件证据收集(per-floor 分组) +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 为事件收集范围内的 EvidenceGroup + * + * 同楼层多个 L0 归入同一组,共享一对 L1。 + * + * @param {object} eventObj - 事件对象 + * @param {object[]} l0Selected - 所有选中的 L0 + * @param {Map} l1ByFloor - 楼层→L1配对映射 + * @param {Set} usedL0Ids - 已消费的 L0 ID 集合(会被修改) + * @returns {EvidenceGroup[]} 该事件的证据组列表(按楼层排序) + */ +function collectEvidenceGroupsForEvent(eventObj, l0Selected, l1ByFloor, usedL0Ids) { + const range = parseFloorRange(eventObj?.summary); + if (!range) return []; + + // 收集范围内未消费的 L0,按楼层分组 + const floorMap = new Map(); + + for (const l0 of l0Selected) { + if (usedL0Ids.has(l0.id)) continue; + if (l0.floor < range.start || l0.floor > range.end) continue; + + if (!floorMap.has(l0.floor)) { + floorMap.set(l0.floor, []); + } + floorMap.get(l0.floor).push(l0); + usedL0Ids.add(l0.id); + } + + // 构建 groups + const groups = []; + for (const [floor, l0s] of floorMap) { + groups.push(buildEvidenceGroup(floor, l0s, l1ByFloor)); + } + + // 按楼层排序 + groups.sort((a, b) => a.floor - b.floor); + + return groups; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 事件格式化(L2 → EvidenceGroup 层级) +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 格式化事件(含 EvidenceGroup 证据) + * @param {object} eventItem - 事件召回项 + * @param {number} idx - 编号 + * @param {EvidenceGroup[]} evidenceGroups - 该事件的证据组 + * @param {Map} causalById - 因果事件索引 + * @returns {string} 格式化后的文本 + */ +function formatEventWithEvidence(eventItem, idx, evidenceGroups, causalById) { + const ev = eventItem?.event || eventItem || {}; + const time = ev.timeLabel || ""; + const title = String(ev.title || "").trim(); + const people = (ev.participants || []).join(" / ").trim(); + const summary = cleanSummary(ev.summary); + + const displayTitle = title || people || ev.id || "事件"; + const header = time ? `${idx}.【${time}】${displayTitle}` : `${idx}. ${displayTitle}`; + + const lines = [header]; + if (people && displayTitle !== people) lines.push(` ${people}`); + lines.push(` ${summary}`); + + // 因果链 + for (const cid of ev.causedBy || []) { + const c = causalById?.get(cid); + if (c) lines.push(formatCausalEventLine(c)); + } + + // EvidenceGroup 证据 + for (const group of evidenceGroups) { + lines.push(...formatEvidenceGroup(group)); + } + + return lines.join("\n"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 非向量模式 +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 构建非向量模式注入文本 + * @param {object} store - 存储对象 + * @returns {string} 注入文本 + */ +function buildNonVectorPrompt(store) { + const data = store.json || {}; + const sections = []; + + // [Constraints] L3 Facts (structured: people/world) + const allFacts = getFacts().filter(f => !f.retracted); + const nonVectorPeopleDict = buildConstraintPeopleDict( + { events: data.events || [] }, + [] + ); + const nonVectorFocus = nonVectorPeopleDict.size > 0 + ? [...nonVectorPeopleDict.values()] + : [...getKnownCharacters(store)]; + const nonVectorKnownCharacters = getKnownCharacters(store); + const filteredConstraints = filterConstraintsByRelevance( + allFacts, + nonVectorFocus, + nonVectorKnownCharacters + ); + const groupedConstraints = groupConstraintsForDisplay(filteredConstraints, nonVectorPeopleDict); + const constraintLines = formatConstraintsStructured(groupedConstraints, 'asc'); + + if (constraintLines.length) { + sections.push(`[定了的事] 已确立的事实\n${constraintLines.join("\n")}`); + } + + // [Events] L2 Events + if (data.events?.length) { + const lines = data.events.map((ev, i) => { + const time = ev.timeLabel || ""; + const title = ev.title || ""; + const people = (ev.participants || []).join(" / "); + const summary = cleanSummary(ev.summary); + const header = time ? `${i + 1}.【${time}】${title || people}` : `${i + 1}. ${title || people}`; + return `${header}\n ${summary}`; + }); + sections.push(`[剧情记忆]\n\n${lines.join("\n\n")}`); + } + + // [Arcs] + if (data.arcs?.length) { + const lines = data.arcs.map(formatArcLine); + sections.push(`[人物弧光]\n${lines.join("\n")}`); + } + + if (!sections.length) return ""; + + return ( + `${buildSystemPreamble()}\n` + + `<剧情记忆>\n\n${sections.join("\n\n")}\n\n\n` + + `${buildPostscript()}` + ); +} + +/** + * 构建非向量模式注入文本(公开接口) + * @returns {string} 注入文本 + */ +export function buildNonVectorPromptText() { + if (!getSettings().storySummary?.enabled) { + return ""; + } + + const store = getSummaryStore(); + if (!store?.json) { + return ""; + } + + let text = buildNonVectorPrompt(store); + if (!text.trim()) { + return ""; + } + + const cfg = getSummaryPanelConfig(); + if (cfg.trigger?.wrapperHead) text = cfg.trigger.wrapperHead + "\n" + text; + if (cfg.trigger?.wrapperTail) text = text + "\n" + cfg.trigger.wrapperTail; + + return text; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 向量模式:预算装配 +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 构建向量模式注入文本 + * @param {object} store - 存储对象 + * @param {object} recallResult - 召回结果 + * @param {Map} causalById - 因果事件索引 + * @param {string[]} focusCharacters - 焦点人物 + * @param {object} meta - 元数据 + * @param {object} metrics - 指标对象 + * @returns {Promise<{promptText: string, injectionStats: object, metrics: object}>} + */ +async function buildVectorPrompt(store, recallResult, causalById, focusCharacters, meta, metrics) { + const T_Start = performance.now(); + + const data = store.json || {}; + const total = { used: 0, max: SHARED_POOL_MAX }; + + // 从 recallResult 解构 + const l0Selected = recallResult?.l0Selected || []; + const l1ByFloor = recallResult?.l1ByFloor || new Map(); + + // 装配结果 + const assembled = { + constraints: { lines: [], tokens: 0 }, + directEvents: { lines: [], tokens: 0 }, + relatedEvents: { lines: [], tokens: 0 }, + distantEvidence: { lines: [], tokens: 0 }, + recentEvidence: { lines: [], tokens: 0 }, + arcs: { lines: [], tokens: 0 }, + }; + + // 注入统计 + const injectionStats = { + budget: { max: SHARED_POOL_MAX + UNSUMMARIZED_EVIDENCE_MAX, used: 0 }, + constraint: { count: 0, tokens: 0, filtered: 0 }, + arc: { count: 0, tokens: 0 }, + event: { selected: 0, tokens: 0 }, + evidence: { l0InEvents: 0, l1InEvents: 0, tokens: 0 }, + distantEvidence: { units: 0, tokens: 0 }, + recentEvidence: { units: 0, tokens: 0 }, + }; + + const eventDetails = { + list: [], + directCount: 0, + relatedCount: 0, + }; + + // 已消费的 L0 ID 集合(事件区域消费后,evidence 区域不再重复) + const usedL0Ids = new Set(); + + // ═══════════════════════════════════════════════════════════════════════ + // [Constraints] L3 Facts → 世界约束 + // ═══════════════════════════════════════════════════════════════════════ + + const T_Constraint_Start = performance.now(); + + const allFacts = getFacts(); + const knownCharacters = getKnownCharacters(store); + const filteredConstraints = filterConstraintsByRelevance(allFacts, focusCharacters, knownCharacters); + const constraintPeopleDict = buildConstraintPeopleDict(recallResult, focusCharacters); + const groupedConstraints = groupConstraintsForDisplay(filteredConstraints, constraintPeopleDict); + + if (metrics) { + metrics.constraint.total = allFacts.length; + metrics.constraint.filtered = allFacts.length - filteredConstraints.length; + } + + const constraintBudget = { used: 0, max: Math.min(CONSTRAINT_MAX, total.max - total.used) }; + const groupedSelectedConstraints = selectConstraintsByBudgetDesc(groupedConstraints, constraintBudget); + const injectedConstraintFacts = (() => { + let count = groupedSelectedConstraints.world.length; + for (const facts of groupedSelectedConstraints.people.values()) { + count += facts.length; + } + return count; + })(); + const constraintLines = formatConstraintsStructured(groupedSelectedConstraints, 'asc'); + + if (constraintLines.length) { + assembled.constraints.lines.push(...constraintLines); + assembled.constraints.tokens = constraintBudget.used; + total.used += constraintBudget.used; + injectionStats.constraint.count = assembled.constraints.lines.length; + injectionStats.constraint.tokens = constraintBudget.used; + injectionStats.constraint.filtered = allFacts.length - filteredConstraints.length; + + if (metrics) { + metrics.constraint.injected = injectedConstraintFacts; + metrics.constraint.tokens = constraintBudget.used; + metrics.constraint.samples = assembled.constraints.lines.slice(0, 3).map(line => + line.length > 60 ? line.slice(0, 60) + '...' : line + ); + metrics.timing.constraintFilter = Math.round(performance.now() - T_Constraint_Start); + } + } else if (metrics) { + metrics.timing.constraintFilter = Math.round(performance.now() - T_Constraint_Start); + } + + // ═══════════════════════════════════════════════════════════════════════ + // [Arcs] 人物弧光 + // ═══════════════════════════════════════════════════════════════════════ + + if (data.arcs?.length && total.used < total.max) { + const { name1 } = getContext(); + const userName = String(name1 || "").trim(); + + const relevant = new Set( + [userName, ...(focusCharacters || [])] + .map(s => String(s || "").trim()) + .filter(Boolean) + ); + + const filteredArcs = (data.arcs || []).filter(a => { + const n = String(a?.name || "").trim(); + return n && relevant.has(n); + }); + + if (filteredArcs.length) { + const arcBudget = { used: 0, max: Math.min(ARCS_MAX, total.max - total.used) }; + for (const a of filteredArcs) { + const line = formatArcLine(a); + if (!pushWithBudget(assembled.arcs.lines, line, arcBudget)) break; + } + assembled.arcs.tokens = arcBudget.used; + total.used += arcBudget.used; + injectionStats.arc.count = assembled.arcs.lines.length; + injectionStats.arc.tokens = arcBudget.used; + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // [Events] L2 Events → 直接命中 + 相似命中 + 因果链 + EvidenceGroup + // ═══════════════════════════════════════════════════════════════════════ + const eventHits = (recallResult?.events || []).filter(e => e?.event?.summary); + + const candidates = [...eventHits].sort((a, b) => (b.similarity || 0) - (a.similarity || 0)); + const eventBudget = { used: 0, max: Math.min(EVENT_BUDGET_MAX, total.max - total.used) }; + const relatedBudget = { used: 0, max: RELATED_EVENT_MAX }; + + const selectedDirect = []; + const selectedRelated = []; + + for (let candidateRank = 0; candidateRank < candidates.length; candidateRank++) { + const e = candidates[candidateRank]; + + if (total.used >= total.max) break; + if (eventBudget.used >= eventBudget.max) break; + + const isDirect = e._recallType === "DIRECT"; + if (!isDirect && relatedBudget.used >= relatedBudget.max) continue; + + // 硬规则:RELATED 事件不挂证据(不挂 L0/L1,只保留事件摘要) + // DIRECT 才允许收集事件内证据组。 + const evidenceGroups = isDirect + ? collectEvidenceGroupsForEvent(e.event, l0Selected, l1ByFloor, usedL0Ids) + : []; + + // 格式化事件(含证据) + const text = formatEventWithEvidence(e, 0, evidenceGroups, causalById); + const cost = estimateTokens(text); + + // 预算检查:整个事件(含证据)作为原子单元 + if (total.used + cost > total.max) { + // 尝试不带证据的版本 + const textNoEvidence = formatEventWithEvidence(e, 0, [], causalById); + const costNoEvidence = estimateTokens(textNoEvidence); + + if (total.used + costNoEvidence > total.max) { + // 归还 usedL0Ids + for (const group of evidenceGroups) { + for (const l0 of group.l0Atoms) { + usedL0Ids.delete(l0.id); + } + } + continue; + } + + // 放入不带证据的版本,归还已消费的 L0 ID + for (const group of evidenceGroups) { + for (const l0 of group.l0Atoms) { + usedL0Ids.delete(l0.id); + } + } + + if (isDirect) { + selectedDirect.push({ + event: e.event, text: textNoEvidence, tokens: costNoEvidence, + evidenceGroups: [], candidateRank, + }); + } else { + selectedRelated.push({ + event: e.event, text: textNoEvidence, tokens: costNoEvidence, + evidenceGroups: [], candidateRank, + }); + } + + injectionStats.event.selected++; + injectionStats.event.tokens += costNoEvidence; + total.used += costNoEvidence; + eventBudget.used += costNoEvidence; + if (!isDirect) relatedBudget.used += costNoEvidence; + + eventDetails.list.push({ + title: e.event?.title || e.event?.id, + isDirect, + hasEvidence: false, + tokens: costNoEvidence, + similarity: e.similarity || 0, + l0Count: 0, + l1FloorCount: 0, + }); + + continue; + } + + // 预算充足,放入完整版本 + let l0Count = 0; + let l1FloorCount = 0; + for (const group of evidenceGroups) { + l0Count += group.l0Atoms.length; + if (group.userL1 || group.aiL1) l1FloorCount++; + } + + if (isDirect) { + selectedDirect.push({ + event: e.event, text, tokens: cost, + evidenceGroups, candidateRank, + }); + } else { + selectedRelated.push({ + event: e.event, text, tokens: cost, + evidenceGroups, candidateRank, + }); + } + + injectionStats.event.selected++; + injectionStats.event.tokens += cost; + injectionStats.evidence.l0InEvents += l0Count; + injectionStats.evidence.l1InEvents += l1FloorCount; + total.used += cost; + eventBudget.used += cost; + if (!isDirect) relatedBudget.used += cost; + + eventDetails.list.push({ + title: e.event?.title || e.event?.id, + isDirect, + hasEvidence: l0Count > 0, + tokens: cost, + similarity: e.similarity || 0, + l0Count, + l1FloorCount, + }); + } + + // 排序 + selectedDirect.sort((a, b) => getEventSortKey(a.event) - getEventSortKey(b.event)); + selectedRelated.sort((a, b) => getEventSortKey(a.event) - getEventSortKey(b.event)); + + // ═══════════════════════════════════════════════════════════════════ + // 邻近补挂:未被事件消费的 L0,距最近已选事件 ≤ 2 楼则补挂 + // 每个 L0 只挂最近的一个事件,不扩展事件范围,不产生重叠 + // ═══════════════════════════════════════════════════════════════════ + + // 重新编号 + 星标 + const directEventTexts = selectedDirect.map((it, i) => { + const numbered = renumberEventText(it.text, i + 1); + return it.candidateRank < TOP_N_STAR ? `⭐${numbered}` : numbered; + }); + + const relatedEventTexts = selectedRelated.map((it, i) => { + const numbered = renumberEventText(it.text, i + 1); + return numbered; + }); + + eventDetails.directCount = selectedDirect.length; + eventDetails.relatedCount = selectedRelated.length; + assembled.directEvents.lines = directEventTexts; + assembled.relatedEvents.lines = relatedEventTexts; + + // ═══════════════════════════════════════════════════════════════════════ + // [Evidence - Distant] 远期证据(已总结范围,未被事件消费的 L0) + // ═══════════════════════════════════════════════════════════════════════ + + const lastSummarized = store.lastSummarizedMesId ?? -1; + const lastChunkFloor = meta?.lastChunkFloor ?? -1; + const keepVisible = store.keepVisibleCount ?? 3; + + // 收集未被事件消费的 L0,按 rerankScore 降序 + const focusSetForEvidence = new Set((focusCharacters || []).map(normalize).filter(Boolean)); + + const remainingL0 = l0Selected + .filter(l0 => !usedL0Ids.has(l0.id)) + .filter(l0 => shouldKeepEvidenceL0(l0, focusSetForEvidence)) + .sort((a, b) => (b.rerankScore || 0) - (a.rerankScore || 0)); + + // 远期:floor <= lastSummarized + const distantL0 = remainingL0.filter(l0 => l0.floor <= lastSummarized); + + if (distantL0.length && total.used < total.max) { + const distantBudget = { used: 0, max: Math.min(SUMMARIZED_EVIDENCE_MAX, total.max - total.used) }; + + // 按楼层排序(时间顺序)后分组 + distantL0.sort((a, b) => a.floor - b.floor); + const distantFloorMap = groupL0ByFloor(distantL0); + + // 按楼层顺序遍历(Map 保持插入顺序,distantL0 已按 floor 排序) + for (const [floor, l0s] of distantFloorMap) { + const group = buildEvidenceGroup(floor, l0s, l1ByFloor); + + // 原子组预算检查 + if (distantBudget.used + group.totalTokens > distantBudget.max) continue; + + const groupLines = formatEvidenceGroup(group); + for (const line of groupLines) { + assembled.distantEvidence.lines.push(line); + } + distantBudget.used += group.totalTokens; + for (const l0 of l0s) { + usedL0Ids.add(l0.id); + } + injectionStats.distantEvidence.units++; + } + + assembled.distantEvidence.tokens = distantBudget.used; + total.used += distantBudget.used; + injectionStats.distantEvidence.tokens = distantBudget.used; + } + + // ═══════════════════════════════════════════════════════════════════════ + // [Evidence - Recent] 近期证据(未总结范围,独立预算) + // ═══════════════════════════════════════════════════════════════════════ + + const recentStart = lastSummarized + 1; + const recentEnd = lastChunkFloor - keepVisible; + + if (recentEnd >= recentStart) { + const recentL0 = remainingL0 + .filter(l0 => !usedL0Ids.has(l0.id)) + .filter(l0 => l0.floor >= recentStart && l0.floor <= recentEnd); + + if (recentL0.length) { + const recentBudget = { used: 0, max: UNSUMMARIZED_EVIDENCE_MAX }; + + // 按楼层排序后分组 + recentL0.sort((a, b) => a.floor - b.floor); + const recentFloorMap = groupL0ByFloor(recentL0); + + for (const [floor, l0s] of recentFloorMap) { + const group = buildEvidenceGroup(floor, l0s, l1ByFloor); + + if (recentBudget.used + group.totalTokens > recentBudget.max) continue; + + const groupLines = formatEvidenceGroup(group); + for (const line of groupLines) { + assembled.recentEvidence.lines.push(line); + } + recentBudget.used += group.totalTokens; + for (const l0 of l0s) { + usedL0Ids.add(l0.id); + } + injectionStats.recentEvidence.units++; + } + + assembled.recentEvidence.tokens = recentBudget.used; + injectionStats.recentEvidence.tokens = recentBudget.used; + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // 按注入顺序拼接 sections + // ═══════════════════════════════════════════════════════════════════════ + + const T_Format_Start = performance.now(); + + const sections = []; + + if (assembled.constraints.lines.length) { + sections.push(`[定了的事] 已确立的事实\n${assembled.constraints.lines.join("\n")}`); + } + if (assembled.directEvents.lines.length) { + sections.push(`[印象深的事] 记得很清楚\n\n${assembled.directEvents.lines.join("\n\n")}`); + } + if (assembled.relatedEvents.lines.length) { + sections.push(`[其他人的事] 别人经历的类似事\n\n${assembled.relatedEvents.lines.join("\n\n")}`); + } + if (assembled.distantEvidence.lines.length) { + sections.push(`[零散记忆] 没归入事件的片段\n${assembled.distantEvidence.lines.join("\n")}`); + } + if (assembled.recentEvidence.lines.length) { + sections.push(`[新鲜记忆] 还没总结的部分\n${assembled.recentEvidence.lines.join("\n")}`); + } + if (assembled.arcs.lines.length) { + sections.push(`[这些人] 他们的弧光\n${assembled.arcs.lines.join("\n")}`); + } + + if (!sections.length) { + if (metrics) { + metrics.timing.evidenceAssembly = Math.round(performance.now() - T_Start - (metrics.timing.constraintFilter || 0)); + metrics.timing.formatting = 0; + } + return { promptText: "", injectionStats, metrics }; + } + + const promptText = + `${buildSystemPreamble()}\n` + + `<剧情记忆>\n\n${sections.join("\n\n")}\n\n\n` + + `${buildPostscript()}`; + + if (metrics) { + metrics.formatting.sectionsIncluded = []; + if (assembled.constraints.lines.length) metrics.formatting.sectionsIncluded.push('constraints'); + if (assembled.directEvents.lines.length) metrics.formatting.sectionsIncluded.push('direct_events'); + if (assembled.relatedEvents.lines.length) metrics.formatting.sectionsIncluded.push('related_events'); + if (assembled.distantEvidence.lines.length) metrics.formatting.sectionsIncluded.push('distant_evidence'); + if (assembled.recentEvidence.lines.length) metrics.formatting.sectionsIncluded.push('recent_evidence'); + if (assembled.arcs.lines.length) metrics.formatting.sectionsIncluded.push('arcs'); + + metrics.formatting.time = Math.round(performance.now() - T_Format_Start); + metrics.timing.formatting = metrics.formatting.time; + + const effectiveTotal = total.used + (assembled.recentEvidence.tokens || 0); + const effectiveLimit = SHARED_POOL_MAX + UNSUMMARIZED_EVIDENCE_MAX; + metrics.budget.total = effectiveTotal; + metrics.budget.limit = effectiveLimit; + metrics.budget.utilization = Math.round(effectiveTotal / effectiveLimit * 100); + metrics.budget.breakdown = { + constraints: assembled.constraints.tokens, + events: injectionStats.event.tokens, + distantEvidence: injectionStats.distantEvidence.tokens, + recentEvidence: injectionStats.recentEvidence.tokens, + arcs: assembled.arcs.tokens, + }; + + metrics.evidence.tokens = injectionStats.distantEvidence.tokens + injectionStats.recentEvidence.tokens; + metrics.evidence.assemblyTime = Math.round( + performance.now() - T_Start - (metrics.timing.constraintFilter || 0) - metrics.formatting.time + ); + metrics.timing.evidenceAssembly = metrics.evidence.assemblyTime; + + const relevantFacts = Math.max(0, allFacts.length - (metrics.constraint.filtered || 0)); + metrics.quality.constraintCoverage = relevantFacts > 0 + ? Math.round((metrics.constraint.injected || 0) / relevantFacts * 100) + : 100; + metrics.quality.eventPrecisionProxy = metrics.event?.similarityDistribution?.mean || 0; + + // l1AttachRate:有 L1 挂载的唯一楼层占所有 L0 覆盖楼层的比例 + const l0Floors = new Set(l0Selected.map(l0 => l0.floor)); + const l0FloorsWithL1 = new Set(); + for (const floor of l0Floors) { + const pair = l1ByFloor.get(floor); + if (pair?.aiTop1 || pair?.userTop1) { + l0FloorsWithL1.add(floor); + } + } + metrics.quality.l1AttachRate = l0Floors.size > 0 + ? Math.round(l0FloorsWithL1.size / l0Floors.size * 100) + : 0; + + metrics.quality.potentialIssues = detectIssues(metrics); + } + + return { promptText, injectionStats, metrics }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 向量模式:召回 + 注入 +// ───────────────────────────────────────────────────────────────────────────── + +/** + * 构建向量模式注入文本(公开接口) + * @param {boolean} excludeLastAi - 是否排除最后的 AI 消息 + * @param {object} hooks - 钩子函数 + * @returns {Promise<{text: string, logText: string}>} + */ +export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) { + const { postToFrame = null, echo = null, pendingUserMessage = null } = hooks; + + if (!getSettings().storySummary?.enabled) { + return { text: "", logText: "" }; + } + + const { chat } = getContext(); + const store = getSummaryStore(); + + if (!store?.json) { + return { text: "", logText: "" }; + } + + const allEvents = store.json.events || []; + const lastIdx = store.lastSummarizedMesId ?? 0; + const length = chat?.length || 0; + + if (lastIdx >= length) { + return { text: "", logText: "" }; + } + + const vectorCfg = getVectorConfig(); + if (!vectorCfg?.enabled) { + return { text: "", logText: "" }; + } + + const { chatId } = getContext(); + const meta = chatId ? await getMeta(chatId) : null; + + let recallResult = null; + let causalById = new Map(); + + try { + recallResult = await recallMemory(allEvents, vectorCfg, { + excludeLastAi, + pendingUserMessage, + }); + + recallResult = { + ...recallResult, + events: recallResult?.events || [], + l0Selected: recallResult?.l0Selected || [], + l1ByFloor: recallResult?.l1ByFloor || new Map(), + causalChain: recallResult?.causalChain || [], + focusTerms: recallResult?.focusTerms || recallResult?.focusEntities || [], + focusEntities: recallResult?.focusTerms || recallResult?.focusEntities || [], // compat alias + focusCharacters: recallResult?.focusCharacters || [], + metrics: recallResult?.metrics || null, + }; + + // 构建因果事件索引 + causalById = new Map( + (recallResult.causalChain || []) + .map(c => [c?.event?.id, c]) + .filter(x => x[0]) + ); + } catch (e) { + xbLog.error(MODULE_ID, "向量召回失败", e); + + if (echo && canNotifyRecallFail()) { + const msg = String(e?.message || "未知错误").replace(/\s+/g, " ").slice(0, 200); + await echo(`/echo severity=warning 嵌入 API 请求失败:${msg}(本次跳过记忆召回)`); + } + + if (postToFrame) { + postToFrame({ + type: "RECALL_LOG", + text: `\n[Vector Recall Failed]\n${String(e?.stack || e?.message || e)}\n`, + }); + } + + return { text: "", logText: `\n[Vector Recall Failed]\n${String(e?.stack || e?.message || e)}\n` }; + } + + const hasUseful = + (recallResult?.events?.length || 0) > 0 || + (recallResult?.l0Selected?.length || 0) > 0 || + (recallResult?.causalChain?.length || 0) > 0; + + if (!hasUseful) { + const noVectorsGenerated = !meta?.fingerprint || (meta?.lastChunkFloor ?? -1) < 0; + const fpMismatch = meta?.fingerprint && meta.fingerprint !== getEngineFingerprint(vectorCfg); + + if (fpMismatch) { + if (echo && canNotifyRecallFail()) { + await echo("/echo severity=warning 向量引擎已变更,请重新生成向量"); + } + } else if (noVectorsGenerated) { + if (echo && canNotifyRecallFail()) { + await echo("/echo severity=warning 没有可用向量,请在剧情总结面板中生成向量"); + } + } + // 向量存在但本次未命中 → 静默跳过,不打扰用户 + + if (postToFrame && (noVectorsGenerated || fpMismatch)) { + postToFrame({ + type: "RECALL_LOG", + text: "\n[Vector Recall Empty]\nNo recall candidates / vectors not ready.\n", + }); + } + return { text: "", logText: "\n[Vector Recall Empty]\nNo recall candidates / vectors not ready.\n" }; + } + + const { promptText, metrics: promptMetrics } = await buildVectorPrompt( + store, + recallResult, + causalById, + recallResult?.focusCharacters || [], + meta, + recallResult?.metrics || null + ); + + const cfg = getSummaryPanelConfig(); + let finalText = String(promptText || ""); + if (cfg.trigger?.wrapperHead) finalText = cfg.trigger.wrapperHead + "\n" + finalText; + if (cfg.trigger?.wrapperTail) finalText = finalText + "\n" + cfg.trigger.wrapperTail; + + const metricsLogText = promptMetrics ? formatMetricsLog(promptMetrics) : ''; + + if (postToFrame) { + postToFrame({ type: "RECALL_LOG", text: metricsLogText }); + } + + return { text: finalText, logText: metricsLogText }; +} diff --git a/modules/story-summary/story-summary-a.css b/modules/story-summary/story-summary-a.css new file mode 100644 index 0000000..83157dc --- /dev/null +++ b/modules/story-summary/story-summary-a.css @@ -0,0 +1,3284 @@ +/* story-summary.css */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-weight: 800; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Facts + ═══════════════════════════════════════════════════════════════════════════ */ + +.facts { + flex: 0 0 auto; +} + +.facts-list { + max-height: 200px; + overflow-y: auto; + padding-right: 4px; +} + +.fact-group { + margin-bottom: 12px; +} + +.fact-group:last-child { + margin-bottom: 0; +} + +.fact-group-title { + font-size: 0.75rem; + color: var(--hl); + margin-bottom: 6px; + padding-bottom: 4px; + border-bottom: 1px dashed var(--bdr2); +} + +.fact-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + margin-bottom: 4px; + background: var(--bg3); + border: 1px solid var(--bdr2); + border-radius: 4px; + font-size: 0.8125rem; +} + +.fact-predicate { + color: var(--txt2); + min-width: 60px; +} + +.fact-predicate::after { + content: ':'; +} + +.fact-object { + color: var(--txt); + flex: 1; +} + +.fact-since { + font-size: 0.625rem; + color: var(--txt3); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Neo-Brutalism Design System + ═══════════════════════════════════════════════════════════════════════════ */ + +:root { + /* ── Base ── */ + --bg: #f0f0f0; + --bg2: #ffffff; + --bg3: #eeeeee; + --txt: #000000; + --txt2: #333333; + --txt3: #555555; + + /* ── Neo-Brutalism Core ── */ + --bdr: #000000; + --bdr2: #000000; + --shadow: 4px 4px 0 var(--txt); + --shadow-hover: 2px 2px 0 var(--txt); + --acc: #000000; + --hl: #ff4444; + --hl2: #d85858; + --hl-soft: #ffeaea; + --inv: #fff; + + /* ── Buttons ── */ + --btn-p-hover: #333; + --btn-p-disabled: #999; + + /* ── Status ── */ + --warn: #ff9800; + --success: #22c55e; + --info: #3b82f6; + --downloading: #f59e0b; + --error: #ef4444; + + /* ── Code blocks ── */ + --code-bg: #1e1e1e; + --code-txt: #d4d4d4; + --muted: #999; + + /* ── Overlay ── */ + --overlay: rgba(0, 0, 0, .5); + + /* ── Tag ── */ + --tag-s-bdr: rgba(255, 68, 68, .2); + --tag-shadow: rgba(0, 0, 0, .12); + + /* ── Category colors ── */ + --cat-status: #e57373; + --cat-inventory: #64b5f6; + --cat-relation: #ba68c8; + --cat-knowledge: #4db6ac; + --cat-rule: #ffd54f; + + /* ── Trend colors ── */ + --trend-broken: #444; + --trend-broken-bg: rgba(68, 68, 68, .15); + --trend-hate: #8b0000; + --trend-hate-bg: rgba(139, 0, 0, .15); + --trend-dislike: #cd5c5c; + --trend-dislike-bg: rgba(205, 92, 92, .15); + --trend-stranger: #888; + --trend-stranger-bg: rgba(136, 136, 136, .15); + --trend-click: #4a9a7e; + --trend-click-bg: rgba(102, 205, 170, .15); + --trend-close-bg: rgba(235, 106, 106, .15); + --trend-merge: #c71585; + --trend-merge-bg: rgba(199, 21, 133, .2); +} + +:root[data-theme="dark"] { + /* ── Base ── */ + --bg: #111111; + --bg2: #222222; + --bg3: #333333; + --txt: #ffffff; + --txt2: #eeeeee; + --txt3: #cccccc; + + /* ── Neo-Brutalism Core ── */ + --bdr: #ffffff; + --bdr2: #ffffff; + --shadow: 4px 4px 0 var(--txt); + --shadow-hover: 2px 2px 0 var(--txt); + --acc: #ffffff; + --hl: #ff6b6b; + --hl2: #e07070; + --hl-soft: #442222; + --inv: #222; + + /* ── Buttons ── */ + --btn-p-hover: #ddd; + --btn-p-disabled: #666; + + /* ── Status ── */ + --warn: #ffb74d; + --success: #4caf50; + --info: #64b5f6; + --downloading: #ffa726; + --error: #ef5350; + + /* ── Code blocks ── */ + --code-bg: #0d0d0d; + --code-txt: #d4d4d4; + --muted: #777; + + /* ── Overlay ── */ + --overlay: rgba(0, 0, 0, .7); + + /* ── Tag ── */ + --tag-s-bdr: rgba(255, 107, 107, .3); + --tag-shadow: rgba(0, 0, 0, .4); + + /* ── Category colors ── */ + --cat-status: #ef9a9a; + --cat-inventory: #90caf9; + --cat-relation: #ce93d8; + --cat-knowledge: #80cbc4; + --cat-rule: #ffe082; + + /* ── Trend colors ── */ + --trend-broken: #999; + --trend-broken-bg: rgba(153, 153, 153, .15); + --trend-hate: #ef5350; + --trend-hate-bg: rgba(239, 83, 80, .15); + --trend-dislike: #e57373; + --trend-dislike-bg: rgba(229, 115, 115, .15); + --trend-stranger: #aaa; + --trend-stranger-bg: rgba(170, 170, 170, .12); + --trend-click: #66bb6a; + --trend-click-bg: rgba(102, 187, 106, .15); + --trend-close-bg: rgba(255, 107, 107, .15); + --trend-merge: #f06292; + --trend-merge-bg: rgba(240, 98, 146, .15); +} + +body { + font-family: 'JetBrains Mono', 'Segoe UI Mono', monospace, -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg); + color: var(--txt); + line-height: 1.5; + min-height: 100vh; + -webkit-overflow-scrolling: touch; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Layout + ═══════════════════════════════════════════════════════════════════════════ */ + +.container { + display: flex; + flex-direction: column; + min-height: 100vh; + padding: 24px 40px; + max-width: 1800px; + margin: 0 auto; +} + +header { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 20px; + border: 2px solid var(--bdr); + background: var(--bg2); + box-shadow: var(--shadow); + margin-bottom: 24px; +} + +main { + display: grid; + grid-template-columns: 1fr 480px; + gap: 24px; + flex: 1; + min-height: 0; +} + +.left, +.right { + display: flex; + flex-direction: column; + gap: 24px; + min-height: 0; +} + +/* Keywords Card */ +.left>.card:first-child { + flex: 0 0 auto; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Typography + ═══════════════════════════════════════════════════════════════════════════ */ + +h1 { + font-size: 2rem; + letter-spacing: -.02em; + margin-bottom: 4px; +} + +.subtitle { + font-size: .875rem; + color: var(--txt3); + letter-spacing: .05em; + text-transform: uppercase; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Stats + ═══════════════════════════════════════════════════════════════════════════ */ + +.stats { + display: flex; + gap: 48px; + text-align: right; +} + +.stat { + display: flex; + flex-direction: column; +} + +.stat-val { + font-size: 2.5rem; + line-height: 1; + letter-spacing: -.03em; +} + +.stat-val .hl { + color: var(--hl); +} + +.stat-lbl { + font-size: .75rem; + color: var(--txt3); + text-transform: uppercase; + letter-spacing: .1em; + margin-top: 4px; +} + +.stat-warning { + font-size: .625rem; + color: var(--warn); + margin-top: 4px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Controls + ═══════════════════════════════════════════════════════════════════════════ */ + +.controls { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 0; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.spacer { + flex: 1; +} + +.chk-label { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + background: transparent; + border: none; + font-size: .8125rem; + color: var(--txt2); + cursor: pointer; + transition: all .2s; +} + +.chk-label:hover { + color: var(--txt); +} + +.chk-label input { + width: 16px; + height: 16px; + accent-color: var(--hl); + cursor: pointer; +} + +.chk-label strong { + color: var(--hl); +} + +#keep-visible-count { + width: 3.5em; + min-width: 3em; + max-width: 4em; + padding: 4px 6px; + margin: 0 4px; + background: var(--bg2); + border: 1px solid var(--bdr); + font-size: inherit; + color: var(--hl); + text-align: center; + border-radius: 3px; + font-variant-numeric: tabular-nums; + + /* Disable number input spinner */ + -moz-appearance: textfield; + appearance: textfield; +} + +#keep-visible-count::-webkit-outer-spin-button, +#keep-visible-count::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +#keep-visible-count:focus { + border-color: var(--acc); + outline: none; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Buttons + ═══════════════════════════════════════════════════════════════════════════ */ + +.btn { + padding: 10px 24px; + background: var(--bg2); + color: var(--txt); + border: 2px solid var(--bdr); + box-shadow: 2px 2px 0 var(--bdr); + font-size: 0.875rem; + cursor: pointer; + transition: transform 0.1s, box-shadow 0.1s; + text-transform: uppercase; + letter-spacing: 0.05em; + border-radius: 0; + /* No rounded corners */ +} + +.btn:hover { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 var(--bdr); + background: var(--bg3); +} + +.btn:active { + transform: translate(2px, 2px); + box-shadow: none; +} + +.btn-p { + background: var(--txt); + color: var(--bg2); + border-color: var(--txt); +} + +.btn-p:hover { + background: var(--txt2); +} + +.btn-p:disabled { + background: var(--txt3); + border-color: var(--txt3); + cursor: not-allowed; + box-shadow: none; + transform: none; + opacity: 0.5; +} + +.btn-icon { + padding: 8px 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.btn-icon svg { + width: 16px; + height: 16px; + stroke-width: 2.5px; +} + +.btn-sm { + padding: 6px 14px; + font-size: 0.75rem; + box-shadow: 2px 2px 0 var(--bdr); +} + +.btn-del { + background: transparent; + color: var(--hl); + border-color: var(--hl); + box-shadow: 2px 2px 0 var(--hl); +} + +.btn-del:hover { + background: var(--hl-soft); + box-shadow: 1px 1px 0 var(--hl); +} + +.btn-group { + display: flex; + gap: 8px; + flex-wrap: nowrap; +} + +.btn-group .btn { + flex: 1; + min-width: 0; + padding: 10px 14px; + text-align: center; + white-space: nowrap; +} + +.btn-group .btn-icon { + padding: 10px 14px; +} + +.btn-debug { + background: var(--bg2); + color: var(--txt2); + border: 1px solid var(--bdr); + display: flex; + align-items: center; + gap: 6px; + justify-content: center; +} + +.btn-debug svg { + width: 14px; + height: 14px; +} + +.btn-debug:hover { + background: var(--bg3); + border-color: var(--acc); + color: var(--txt); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Cards & Sections + ═══════════════════════════════════════════════════════════════════════════ */ + +.card { + background: var(--bg2); + border: 2px solid var(--bdr); + box-shadow: var(--shadow); + padding: 24px; + border-radius: 4px; + margin-bottom: 1em; +} + +/* Reuse the Neo-Brutalism card logic for all cards */ +.card:hover { + transform: translate(1px, 1px); + box-shadow: var(--shadow-hover); +} + +.sec-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 2px solid var(--bdr); + padding-bottom: 8px; +} + +.sec-title { + font-size: 0.875rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--txt); + background: var(--bg2); + display: flex; + align-items: flex-end; + gap: 1em; +} + +.sec-btn { + padding: 4px 8px; + background: var(--bg2); + border: 2px solid var(--bdr); + font-size: 0.75rem; + color: var(--txt); + cursor: pointer; + box-shadow: 2px 2px 0 var(--bdr); + transition: all 0.1s; +} + +.sec-btn:hover { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 var(--bdr); +} + +.sec-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.sec-icon { + padding: 4px 8px; + display: flex; + align-items: center; + justify-content: center; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Keywords & Tags + ═══════════════════════════════════════════════════════════════════════════ */ + +.keywords { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 16px; +} + +.tag { + padding: 6px 16px; + background: var(--bg2); + border: 2px solid var(--bdr); + font-size: 0.8125rem; + color: var(--txt); + transition: transform 0.1s, box-shadow 0.1s; + cursor: default; + box-shadow: 2px 2px 0 var(--bdr); +} + +.tag.p { + background: var(--txt); + color: var(--bg2); + border-color: var(--txt); +} + +.tag.s { + background: var(--bg2); + border-color: var(--hl); + color: var(--hl); + box-shadow: 2px 2px 0 var(--hl); +} + +.tag:hover { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 var(--bdr); +} + +.tag.s:hover { + box-shadow: 1px 1px 0 var(--hl); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Timeline + ═══════════════════════════════════════════════════════════════════════════ */ + +.timeline { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + max-height: 1140px; +} + +.tl-list { + flex: 1; + overflow-y: auto; + padding-right: 8px; + min-height: 0; +} + +.tl-list::-webkit-scrollbar, +.scroll::-webkit-scrollbar { + width: 4px; +} + +.tl-list::-webkit-scrollbar-thumb, +.scroll::-webkit-scrollbar-thumb { + background: var(--bdr); +} + +.tl-item { + position: relative; + padding-left: 32px; + padding-bottom: 32px; + border-left: 1px solid var(--bdr); + margin-left: 8px; +} + +.tl-item:last-child { + border-left-color: transparent; + padding-bottom: 0; +} + +.tl-dot { + position: absolute; + left: -5px; + top: 0; + width: 9px; + height: 9px; + background: var(--bg2); + border: 2px solid var(--txt3); + border-radius: 50%; + transition: all .2s; +} + +.tl-item:hover .tl-dot { + border-color: var(--hl); + background: var(--hl); + transform: scale(1.3); +} + +.tl-item.crit .tl-dot { + border-color: var(--hl); + background: var(--hl); +} + +.tl-head { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 8px; +} + +.tl-title { + font-size: 1rem; +} + +.tl-time { + font-size: .75rem; + color: var(--txt3); + font-variant-numeric: tabular-nums; +} + +.tl-brief { + font-size: .875rem; + color: var(--txt2); + line-height: 1.7; + margin-bottom: 12px; +} + +.tl-meta { + display: flex; + gap: 16px; + font-size: .75rem; + color: var(--txt3); +} + +.tl-meta .imp { + color: var(--hl); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Relations Chart + ═══════════════════════════════════════════════════════════════════════════ */ + +.relations { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + max-height: 480px; +} + +#relation-chart, +#relation-chart-fullscreen { + width: 100%; + flex: 1; + min-height: 0; + touch-action: none; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Profile + ═══════════════════════════════════════════════════════════════════════════ */ + +.profile { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + max-height: 480px; +} + +.profile-content { + flex: 1; + overflow-y: auto; + padding-right: 8px; + min-height: 0; +} + +.prof-arc { + padding: 16px; + margin-bottom: 24px; +} + +.prof-name { + font-size: 1.125rem; + margin-bottom: 4px; +} + +.prof-traj { + font-size: .8125rem; + color: var(--txt3); + line-height: 1.5; +} + +.prof-prog-wrap { + margin-bottom: 16px; +} + +.prof-prog-lbl { + display: flex; + justify-content: space-between; + font-size: .75rem; + color: var(--txt3); + margin-bottom: 6px; +} + +.prof-prog { + height: 4px; + background: var(--bdr); + border-radius: 2px; + overflow: hidden; +} + +.prof-prog-inner { + height: 100%; + background: linear-gradient(90deg, var(--hl), var(--hl2)); + border-radius: 2px; + transition: width .6s; +} + +.prof-moments { + background: var(--bg2); + border-left: 3px solid var(--hl); + padding: 12px 16px; +} + +.prof-moments-title { + font-size: .6875rem; + text-transform: uppercase; + letter-spacing: .1em; + color: var(--txt3); + margin-bottom: 8px; +} + +.prof-moment { + position: relative; + padding-left: 16px; + margin-bottom: 6px; + font-size: .8125rem; + color: var(--txt2); + line-height: 1.5; +} + +.prof-moment::before { + content: ''; + position: absolute; + left: 0; + top: 7px; + width: 6px; + height: 6px; + background: var(--hl); + border-radius: 50%; +} + +.prof-moment:last-child { + margin-bottom: 0; +} + +.prof-rels { + display: flex; + flex-direction: column; +} + +.rels-group { + border-bottom: 1px solid var(--bdr2); + padding: 16px 0; +} + +.rels-group:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.rels-group:first-child { + padding-top: 0; +} + +.rels-group-title { + font-size: .75rem; + color: var(--txt3); + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.rel-item { + display: flex; + align-items: baseline; + gap: 8px; + padding: 4px 8px; + border-radius: 4px; + margin-bottom: 2px; +} + +.rel-item:hover { + background: var(--bg3); +} + +.rel-target { + font-size: .9rem; + color: var(--txt2); + white-space: nowrap; + min-width: 60px; +} + +.rel-label { + font-size: .7rem; + line-height: 1.5; + flex: 1; +} + +.rel-trend { + font-size: .6875rem; + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; +} + +.trend-broken { + background: var(--trend-broken-bg); + color: var(--trend-broken); +} + +.trend-hate { + background: var(--trend-hate-bg); + color: var(--trend-hate); +} + +.trend-dislike { + background: var(--trend-dislike-bg); + color: var(--trend-dislike); +} + +.trend-stranger { + background: var(--trend-stranger-bg); + color: var(--trend-stranger); +} + +.trend-click { + background: var(--trend-click-bg); + color: var(--trend-click); +} + +.trend-close { + background: var(--trend-close-bg); + color: var(--hl); +} + +.trend-merge { + background: var(--trend-merge-bg); + color: var(--trend-merge); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Custom Select + ═══════════════════════════════════════════════════════════════════════════ */ + +.custom-select { + position: relative; + min-width: 140px; + font-size: .8125rem; +} + +.sel-trigger { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + background: var(--bg3); + border: 1px solid var(--bdr); + border-radius: 6px; + cursor: pointer; + transition: all .2s; + user-select: none; +} + +.sel-trigger:hover { + border-color: var(--acc); + background: var(--bg2); +} + +.sel-trigger::after { + content: ''; + width: 16px; + height: 16px; + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238d8d8d' stroke-width='2'%3e%3cpath d='M6 9l6 6 6-6'/%3e%3c/svg%3e") center/16px no-repeat; + transition: transform .2s; +} + +.custom-select.open .sel-trigger::after { + transform: rotate(180deg); +} + +.sel-opts { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background: var(--bg2); + border: 1px solid var(--bdr); + border-radius: 6px; + box-shadow: 0 4px 20px rgba(0, 0, 0, .15); + z-index: 100; + display: none; + max-height: 240px; + overflow-y: auto; + padding: 4px; +} + +.custom-select.open .sel-opts { + display: block; + animation: fadeIn .2s; +} + +.sel-opt { + padding: 8px 12px; + cursor: pointer; + border-radius: 4px; + transition: background .1s; +} + +.sel-opt:hover { + background: var(--bg3); +} + +.sel-opt.sel { + background: var(--hl-soft); + color: var(--hl); +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Modal + ═══════════════════════════════════════════════════════════════════════════ */ + +.modal { + position: fixed; + inset: 0; + z-index: 10000; + display: none; + align-items: center; + justify-content: center; +} + +.modal.active { + display: flex; +} + +.modal-bg { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(2px); +} + +.modal-box { + position: relative; + width: 100%; + max-width: 720px; + max-height: 90vh; + background: var(--bg2); + border: 2px solid var(--bdr); + box-shadow: 8px 8px 0 var(--bdr); + overflow: hidden; + display: flex; + flex-direction: column; + border-radius: 4px; +} + +.modal-head { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 2px solid var(--bdr); + background: var(--bg2); + position: relative; +} + +.modal-head h2 { + font-size: 1rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.modal-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg2); + border: 2px solid var(--bdr); + cursor: pointer; + transition: transform 0.1s, box-shadow 0.1s; + box-shadow: 2px 2px 0 var(--bdr); +} + +.modal-close:hover { + background: var(--hl); + border-color: var(--bdr); + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 var(--bdr); +} + +.modal-close:hover svg { + stroke: var(--inv); +} + +.modal-close svg { + width: 16px; + height: 16px; + stroke: var(--txt); + stroke-width: 3px; +} + +.modal-body { + flex: 1; + overflow-y: auto; + padding: 24px; +} + +.modal-foot { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 24px; + border-top: 2px solid var(--bdr); + background: var(--bg2); +} + +.fullscreen .modal-box { + width: 95vw; + height: 90vh; + max-width: none; + max-height: none; +} + +.fullscreen .modal-body { + flex: 1; + padding: 0; + overflow: hidden; +} + +#relation-chart-fullscreen { + width: 100%; + height: 100%; + min-height: 500px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Editor + ═══════════════════════════════════════════════════════════════════════════ */ + +.editor-ta { + width: 100%; + min-height: 300px; + padding: 16px; + background: var(--bg3); + border: 1px solid var(--bdr); + font-family: 'SF Mono', Monaco, Consolas, monospace; + font-size: .8125rem; + line-height: 1.6; + color: var(--txt); + resize: vertical; + outline: none; +} + +.editor-ta:focus { + border-color: var(--acc); +} + +.editor-hint { + font-size: .75rem; + color: var(--txt3); + margin-bottom: 12px; + line-height: 1.5; +} + +.editor-err { + padding: 12px; + background: var(--hl-soft); + border: 1px solid rgba(255, 68, 68, .3); + color: var(--hl); + font-size: .8125rem; + margin-top: 12px; + display: none; +} + +.editor-err.visible { + display: block; +} + +.struct-item { + border: 1px solid var(--bdr); + background: var(--bg3); + padding: 12px; + margin-bottom: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.struct-row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.struct-row input, +.struct-row select, +.struct-row textarea { + flex: 1; + min-width: 0; + padding: 8px 10px; + background: var(--bg2); + border: 1px solid var(--bdr); + font-size: .8125rem; + color: var(--txt); + outline: none; + transition: border-color .2s; +} + +.struct-row input:focus, +.struct-row select:focus, +.struct-row textarea:focus { + border-color: var(--acc); +} + +.struct-row textarea { + resize: vertical; + font-family: inherit; + min-height: 60px; +} + +.struct-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 4px; +} + +.struct-actions span { + font-size: .75rem; + color: var(--txt3); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Settings + ═══════════════════════════════════════════════════════════════════════════ */ + +.settings-section { + margin-bottom: 32px; +} + +.settings-section:last-child { + margin-bottom: 0; +} + +.settings-section-title { + font-size: .6875rem; + text-transform: uppercase; + letter-spacing: .15em; + color: var(--txt3); + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--bdr2); +} + +.settings-row { + display: flex; + gap: 16px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.settings-row:last-child { + margin-bottom: 0; +} + +.settings-field { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 200px; +} + +.settings-field.full { + flex: 100%; +} + +.settings-field label { + font-size: .75rem; + color: var(--txt3); + text-transform: uppercase; + letter-spacing: .05em; +} + +.settings-field input:not([type="checkbox"]):not([type="radio"]), +.settings-field select { + width: 100%; + max-width: 100%; + padding: 10px 14px; + background: var(--bg2); + border: 2px solid var(--bdr); + font-size: .875rem; + color: var(--txt); + outline: none; + transition: all 0.1s; + box-sizing: border-box; + border-radius: 0; +} + +.settings-field input[type="checkbox"], +.settings-field input[type="radio"] { + width: auto; + height: auto; + accent-color: var(--txt); +} + +.settings-field select { + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='square' stroke-linejoin='miter'%3E%3Cpolyline points='2 4 6 8 10 4'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 14px center; + padding-right: 32px; +} + +@media (prefers-color-scheme: dark) { + .settings-field select { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='square' stroke-linejoin='miter'%3E%3Cpolyline points='2 4 6 8 10 4'%3E%3C/polyline%3E%3C/svg%3E"); + } +} + +.settings-field input:focus, +.settings-field select:focus { + border-color: var(--acc); + box-shadow: 4px 4px 0 var(--acc); +} + +.settings-field input[type="password"] { + letter-spacing: .15em; + font-family: monospace; +} + +.settings-field-inline { + display: flex; + align-items: center; + gap: 8px; +} + +.settings-field-inline input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--acc); +} + +.settings-field-inline label { + font-size: .8125rem; + color: var(--txt2); + text-transform: none; + letter-spacing: 0; +} + +.settings-hint { + font-size: .75rem; + color: var(--txt3); + margin-top: 4px; +} + +.settings-btn-row { + display: flex; + gap: 12px; + margin-top: 8px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Vector Settings + ═══════════════════════════════════════════════════════════════════════════ */ + +.engine-selector { + display: flex; + gap: 16px; + margin-top: 8px; +} + +.engine-option { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: .875rem; + color: var(--txt2); +} + +.engine-option input { + accent-color: var(--hl); + width: 18px; + height: 18px; + margin: 0; + cursor: pointer; +} + +.engine-area { + margin-top: 12px; + padding: 16px; + background: var(--bg3); + border: 1px solid var(--bdr); +} + +.engine-card { + text-align: center; +} + +.engine-card-title { + font-size: 1rem; + margin-bottom: 4px; +} + +.engine-status-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-top: 12px; +} + +.engine-status { + display: flex; + align-items: center; + gap: 6px; + font-size: .8125rem; + color: var(--txt3); + flex: 1; +} + +.engine-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + flex: 2; +} + +/* Test connection button sizing */ +#btn-test-vector-api { + flex: 2; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--txt3); +} + +.status-dot.ready { + background: var(--success); +} + +.status-dot.cached { + background: var(--info); +} + +.status-dot.downloading { + background: var(--downloading); + animation: pulse 1s infinite; +} + +.status-dot.error { + background: var(--error); +} + +.status-dot.success { + background: var(--success); +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: .5; + } +} + +.engine-progress { + margin: 12px 0; +} + +.progress-bar { + height: 6px; + background: var(--bdr); + border-radius: 3px; + overflow: hidden; +} + +.progress-inner { + height: 100%; + background: linear-gradient(90deg, var(--hl), var(--hl2)); + border-radius: 3px; + width: 0%; + transition: width .3s; +} + +.progress-text { + font-size: .75rem; + color: var(--txt3); + display: block; + text-align: center; + margin-top: 4px; +} + +.model-select-row { + display: flex; + gap: 8px; + align-items: center; + margin-bottom: 12px; +} + +.model-select-row select { + flex: 1; + padding: 8px 12px; + background: var(--bg2); + border: 1px solid var(--bdr); + font-size: .875rem; + color: var(--txt); +} + +.model-desc { + font-size: .75rem; + color: var(--txt3); + text-align: left; + margin-bottom: 4px; +} + +.vector-mismatch-warning { + font-size: .75rem; + color: var(--downloading); + margin-top: 6px; +} + +.vector-chat-section { + border-top: 1px solid var(--bdr); + padding-top: 16px; + margin-top: 16px; +} + +#vector-action-row { + display: flex; + gap: 8px; + justify-content: center; + width: 100%; +} + +#vector-action-row .btn { + flex: 1; + min-width: 0; +} + +.provider-hint { + font-size: .75rem; + color: var(--txt3); + margin-top: 4px; +} + +.provider-hint a { + color: var(--hl); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Recall Log + ═══════════════════════════════════════════════════════════════════════════ */ + +#recall-log-modal .modal-box { + max-width: 900px; + display: flex; + flex-direction: column; +} + +#recall-log-modal .modal-body { + flex: 1; + min-height: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +#recall-log-content { + font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; + font-size: 12px; + line-height: 1.6; + color: var(--code-txt); + white-space: pre-wrap !important; + overflow-x: hidden !important; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + HF Guide + ═══════════════════════════════════════════════════════════════════════════ */ + +.hf-guide { + font-size: .875rem; + line-height: 1.7; +} + +.hf-section { + margin-bottom: 28px; + padding-bottom: 24px; + border-bottom: 1px solid var(--bdr2); +} + +.hf-section:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.hf-intro { + background: linear-gradient(135deg, rgba(102, 126, 234, .08), rgba(118, 75, 162, .08)); + border: 1px solid rgba(102, 126, 234, .2); + border-radius: 8px; + padding: 20px; + text-align: center; + border-bottom: none; +} + +.hf-intro-text { + font-size: 1.1rem; + margin-bottom: 12px; +} + +.hf-intro-badges { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} + +.hf-badge { + padding: 4px 12px; + background: var(--bg2); + border: 1px solid var(--bdr); + border-radius: 20px; + font-size: .75rem; + color: var(--txt2); +} + +.hf-step-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.hf-step-num { + width: 28px; + height: 28px; + background: var(--acc); + color: var(--inv); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: .875rem; + flex-shrink: 0; +} + +.hf-step-title { + font-size: 1rem; + color: var(--txt); +} + +.hf-step-content { + padding-left: 40px; +} + +.hf-step-content p { + margin: 0 0 12px; +} + +.hf-step-content a { + color: var(--hl); + text-decoration: none; +} + +.hf-step-content a:hover { + text-decoration: underline; +} + +.hf-checklist { + margin: 12px 0; + padding-left: 20px; +} + +.hf-checklist li { + margin-bottom: 6px; +} + +.hf-checklist li::marker { + color: var(--hl); +} + +.hf-checklist code, +.hf-faq code { + background: var(--bg3); + padding: 2px 6px; + border-radius: 3px; + font-size: .8125rem; +} + +.hf-file { + margin-bottom: 16px; + border: 1px solid var(--bdr); + border-radius: 6px; + overflow: hidden; +} + +.hf-file:last-child { + margin-bottom: 0; +} + +.hf-file-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: var(--bg3); + border-bottom: 1px solid var(--bdr); + font-size: .8125rem; +} + +.hf-file-icon { + font-size: 1rem; +} + +.hf-file-name { + font-family: 'SF Mono', Monaco, Consolas, monospace; +} + +.hf-file-note { + color: var(--txt3); + font-size: .75rem; + margin-left: auto; +} + +.hf-code { + margin: 0; + padding: 14px; + background: var(--code-bg); + overflow-x: auto; + position: relative; +} + +.hf-code code { + font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; + font-size: .75rem; + line-height: 1.5; + color: var(--code-txt); + display: block; + white-space: pre; +} + +.hf-code .copy-btn { + position: absolute; + right: 8px; + top: 8px; + padding: 4px 10px; + background: rgba(255, 255, 255, .1); + border: 1px solid rgba(255, 255, 255, .2); + color: var(--muted); + font-size: .6875rem; + cursor: pointer; + border-radius: 4px; + transition: all .2s; +} + +.hf-code .copy-btn:hover { + background: rgba(255, 255, 255, .2); + color: var(--inv); +} + +.hf-status-badge { + display: inline-block; + padding: 2px 10px; + background: rgba(34, 197, 94, .15); + color: var(--success); + border-radius: 10px; + font-size: .75rem; +} + +.hf-config-table { + background: var(--bg3); + border: 1px solid var(--bdr); + border-radius: 6px; + overflow: hidden; +} + +.hf-config-row { + display: flex; + padding: 12px 16px; + border-bottom: 1px solid var(--bdr); +} + +.hf-config-row:last-child { + border-bottom: none; +} + +.hf-config-label { + width: 100px; + flex-shrink: 0; + color: var(--txt2); +} + +.hf-config-value { + flex: 1; + color: var(--txt); +} + +.hf-config-value code { + background: var(--bg2); + padding: 2px 6px; + border-radius: 3px; + font-size: .8125rem; + word-break: break-all; +} + +.hf-faq { + background: var(--bg3); + border: 1px solid var(--bdr); + border-radius: 6px; + padding: 16px 20px; + border-bottom: none; +} + +.hf-faq-title { + margin-bottom: 12px; + color: var(--txt); +} + +.hf-faq ul { + margin: 0; + padding-left: 20px; +} + +.hf-faq li { + margin-bottom: 8px; + color: var(--txt2); +} + +.hf-faq li:last-child { + margin-bottom: 0; +} + +.hf-faq a { + color: var(--hl); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Utilities + ═══════════════════════════════════════════════════════════════════════════ */ + +.hidden { + display: none !important; +} + +.empty { + text-align: center; + padding: 40px; + color: var(--txt3); + font-size: .875rem; +} + + +/* ═══════════════════════════════════════════════════════════════════════════ + World State (L3) + ═══════════════════════════════════════════════════════════════════════════ */ + +.world-state { + flex: 0 0 auto; +} + +.world-state-list { + max-height: 200px; + overflow-y: auto; + padding-right: 4px; +} + +.world-group { + margin-bottom: 16px; +} + +.world-group:last-child { + margin-bottom: 0; +} + +.world-group-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--txt3); + margin-bottom: 8px; + padding-bottom: 6px; + border-bottom: 1px solid var(--bdr2); +} + +.world-item { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 8px 10px; + margin-bottom: 6px; + background: var(--bg3); + border: 1px solid var(--bdr2); + border-radius: 6px; + font-size: 0.8125rem; + transition: all 0.15s ease; +} + +.world-item:hover { + border-color: var(--bdr); + background: var(--bg2); +} + +.world-item:last-child { + margin-bottom: 0; +} + +.world-topic { + color: var(--txt); + white-space: nowrap; + flex-shrink: 0; +} + +.world-content { + color: var(--txt2); + flex: 1; + line-height: 1.5; +} + +/* Category Icon Colors */ +.world-group[data-category="status"] .world-group-title { + color: var(--cat-status); +} + +.world-group[data-category="inventory"] .world-group-title { + color: var(--cat-inventory); +} + +.world-group[data-category="relation"] .world-group-title { + color: var(--cat-relation); +} + +.world-group[data-category="knowledge"] .world-group-title { + color: var(--cat-knowledge); +} + +.world-group[data-category="rule"] .world-group-title { + color: var(--cat-rule); +} + +/* Empty State */ +.world-state-list .empty { + padding: 24px; + font-size: 0.8125rem; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Settings (Tabbed Modal) + ═══════════════════════════════════════════════════════════════════════════ */ + +.settings-modal-box { + max-width: 680px; +} + +/* Collapsible Section */ +.settings-collapse { + margin-top: 20px; + border-radius: 8px; + overflow: hidden; +} + +.settings-collapse-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + cursor: pointer; + font-size: .8125rem; + color: var(--txt2); + background: var(--bg3); + border: 2px solid var(--bdr); + border-radius: 6px; +} + +.collapse-icon { + width: 16px; + height: 16px; + transition: transform .2s; +} + +.settings-collapse.open .collapse-icon { + transform: rotate(180deg); +} + +.settings-collapse-content { + padding: 16px; + border-top: 1px solid var(--bdr); +} + +/* Checkbox Group */ +.settings-checkbox-group { + margin-bottom: 20px; + padding: 0; + background: transparent; + border: none; +} + +.settings-checkbox-group:last-child { + margin-bottom: 0; +} + +.settings-checkbox { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + user-select: none; +} + +.settings-checkbox input[type="checkbox"] { + display: none; +} + +.checkbox-mark { + width: 20px; + height: 20px; + border: 2px solid var(--bdr); + border-radius: 4px; + background: var(--bg2); + position: relative; + transition: all .2s; + flex-shrink: 0; +} + +.settings-checkbox input:checked+.checkbox-mark { + background: var(--acc); + border-color: var(--acc); +} + +.settings-checkbox input:checked+.checkbox-mark::after { + content: ''; + position: absolute; + left: 6px; + top: 2px; + width: 5px; + height: 10px; + border: solid var(--inv); + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.checkbox-label { + font-size: .875rem; + color: var(--txt); +} + +.settings-checkbox-group .settings-hint { + margin-left: 30px; + margin-top: 4px; +} + +/* Sub Options */ +.settings-sub-options { + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed var(--bdr); +} + +/* Filter Rules */ +.filter-rules-section { + margin-top: 20px; + padding: 16px; + background: var(--bg2); + border: 2px solid var(--bdr); + border-radius: 4px; + box-shadow: 4px 4px 0 var(--bdr); +} + +.filter-rules-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + gap: 12px; + border-bottom: 2px solid var(--bdr); + padding-bottom: 12px; +} + +.filter-rules-header label { + font-size: .75rem; + color: var(--txt); + text-transform: uppercase; + letter-spacing: .05em; + font-weight: 800; + flex: 1; +} + +.btn-add { + flex: 2; + justify-content: center; + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; +} + +.filter-rules-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 12px; +} + +.filter-rule-item { + display: flex; + gap: 8px; + align-items: flex-start; + padding: 12px; + background: var(--bg2); + border: 2px solid var(--bdr); + border-radius: 4px; + box-shadow: 2px 2px 0 var(--bdr); +} + +.filter-rule-inputs { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + flex: 1; +} + +.filter-rule-item input { + width: 100%; + padding: 8px 10px; + background: var(--bg3); + border: 2px solid var(--bdr); + font-size: .8125rem; + color: var(--txt); + border-radius: 0; + transition: all 0.1s; +} + +.filter-rule-item input:focus { + border-color: var(--acc); + outline: none; + box-shadow: 2px 2px 0 var(--acc); +} + +.filter-rule-item .rule-arrow { + color: var(--txt); + font-size: 1rem; + font-weight: 800; + flex-shrink: 0; + padding: 2px 0; +} + +.filter-rule-item .btn-del-rule { + padding: 6px 10px; + background: transparent; + border: 2px solid var(--hl); + color: var(--hl); + cursor: pointer; + border-radius: 0; + font-size: .75rem; + transition: all .2s; + flex-shrink: 0; + align-self: center; + box-shadow: 2px 2px 0 var(--hl); +} + +.filter-rule-item .btn-del-rule:hover { + background: var(--hl-soft); + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 var(--hl); +} + +/* Vector Stats - Original horizontal layout */ +.vector-stats { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 16px; + font-size: .875rem; + color: var(--txt2); + margin-top: 8px; +} + +.vector-stat-col { + display: flex; + align-items: center; +} + +.vector-stat-label { + font-size: .75rem; + color: var(--txt3); +} + +.vector-stat-value { + color: var(--txt2); +} + +.vector-stat-value strong { + color: var(--hl); +} + +.vector-stat-sep { + color: var(--txt3); + align-self: center; +} + +.vector-io-section { + border-top: 1px solid var(--bdr); + padding-top: 16px; + margin-top: 16px; +} + +/* Settings Tabs */ +.settings-tabs { + display: flex; + gap: 12px; + align-self: flex-end; + margin-bottom: -22px; + /* Pull down to sit on the border */ + position: relative; + z-index: 10; +} + +.settings-tab { + font-size: .8125rem; + color: var(--txt3); + cursor: pointer; + padding: 8px 16px; + border: 2px solid transparent; + border-bottom: none; + transition: all .1s; + user-select: none; + text-transform: uppercase; + letter-spacing: .05em; + font-weight: 800; + background: transparent; +} + +.settings-tab:hover { + color: var(--txt); +} + +.settings-tab.active { + color: var(--txt); + background: var(--bg2); + border: 2px solid var(--bdr); + border-bottom: 2px solid var(--bg2); +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; + animation: fadeIn .3s ease; +} + +.debug-log-header { + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px dashed var(--bdr2); +} + +.debug-title { + font-size: .875rem; + color: var(--txt); + margin-bottom: 4px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Recall Log / Debug Log + ═══════════════════════════════════════════════════════════════════════════ */ + +.debug-log-viewer { + background: var(--code-bg); + color: var(--code-txt); + padding: 16px; + border-radius: 8px; + font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; + font-size: 12px; + line-height: 1.6; + max-height: 60vh; + overflow-y: auto; + overflow-x: hidden; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: break-word; +} + +.recall-empty { + color: var(--muted); + text-align: center; + padding: 40px; + font-style: italic; + font-size: .8125rem; + line-height: 1.8; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Metrics Log Styling + ═══════════════════════════════════════════════════════════════════════════ */ + +#recall-log-content .metric-warn { + color: var(--downloading); +} + +#recall-log-content .metric-error { + color: var(--error); +} + +#recall-log-content .metric-good { + color: var(--success); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Guide Styles (Neo-Brutalism) + ═══════════════════════════════════════════════════════════════════════════ */ + +.guide-container { + max-width: 800px; + margin: 0 auto; + padding: 0 16px 40px; +} + +.guide-section { + margin-bottom: 48px; +} + +.guide-title { + display: flex; + align-items: center; + gap: 12px; + font-size: 1.25rem; + font-weight: 800; + margin-bottom: 20px; +} + +.guide-num { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: var(--txt); + color: var(--bg2); + font-size: 1.125rem; + font-family: monospace; + border-radius: 0; + box-shadow: 4px 4px 0 var(--bdr); +} + +.guide-steps { + display: flex; + flex-direction: column; + gap: 20px; +} + +.guide-step { + display: flex; + gap: 16px; + background: var(--bg2); + border: 2px solid var(--bdr); + padding: 20px; + box-shadow: 4px 4px 0 var(--bdr); +} + +.guide-step-num { + flex-shrink: 0; + width: 24px; + height: 24px; + background: var(--bg3); + border: 2px solid var(--bdr); + color: var(--txt); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + font-weight: 800; +} + +.guide-step-body { + flex: 1; +} + +.guide-step-title { + font-size: 1rem; + margin-bottom: 6px; +} + +.guide-step-desc { + font-size: 0.875rem; + color: var(--txt2); + line-height: 1.6; +} + +.guide-card-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 20px; +} + +.guide-card { + background: var(--bg2); + border: 2px solid var(--bdr); + padding: 16px; + box-shadow: 4px 4px 0 var(--bdr); +} + +.guide-card:hover { + transform: translate(1px, 1px); + box-shadow: 2px 2px 0 var(--bdr); +} + +.guide-card-title { + font-size: 0.9375rem; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 8px; +} + +.guide-card-desc { + font-size: 0.8125rem; + color: var(--txt2); + line-height: 1.5; +} + +/* Guide Tips */ +.guide-tips-list { + display: grid; + gap: 12px; +} + +.guide-tip { + display: flex; + gap: 12px; + padding: 12px 16px; + background: var(--bg3); + border: 2px solid var(--bdr); + align-items: flex-start; +} + +.guide-tip-icon { + font-size: 1.25rem; +} + +.guide-tip-text { + font-size: 0.875rem; + color: var(--txt2); + line-height: 1.5; +} + +/* Guide FAQ */ +.guide-faq-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.guide-faq-item { + border: 2px solid var(--bdr); + padding: 16px; + background: var(--bg2); +} + +.guide-faq-q { + font-size: 0.9375rem; + margin-bottom: 8px; + color: var(--txt); +} + +.guide-faq-a { + font-size: 0.875rem; + color: var(--txt2); + line-height: 1.6; +} + +.guide-highlight { + background: var(--bg2); + border: 2px solid var(--bdr); + padding: 20px; + position: relative; +} + +.guide-highlight::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 6px; + background: var(--hl); + border-right: 2px solid var(--bdr); +} + +.guide-list { + list-style: none; + padding: 0; + margin: 12px 0 20px; +} + +.guide-list li { + margin-bottom: 10px; + padding-left: 20px; + position: relative; + line-height: 1.6; +} + +.guide-list li::before { + content: '▪'; + position: absolute; + left: 0; + top: 0; + color: var(--hl); +} + +.guide-list-inner { + list-style: none; + padding: 8px 0 0 0; + margin: 0; +} + +.guide-list-inner li { + padding-left: 18px; + margin-bottom: 6px; + font-size: 0.8125rem; +} + +.guide-list-inner li::before { + content: '○'; + font-size: 0.75rem; + color: var(--txt3); + top: 1px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Tools + ═══════════════════════════════════════════════════════════════════════════ */ +.neo-tools-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 32px; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 2px dashed var(--bdr); + color: var(--txt3); + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.1em; +} + +.neo-badge { + /* Explicitly requested Black Background & White Text */ + background: var(--acc); + color: var(--inv); + padding: 2px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.05em; + display: inline-block; + vertical-align: middle; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Consolidated Responsive Design + ═══════════════════════════════════════════════════════════════════════════ */ + +/* Tablet (Laptop/Narrow PC) */ +@media (max-width: 1200px) { + .container { + padding: 16px 24px; + } + + main { + grid-template-columns: 1fr; + } + + .right { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + } + + .relations, + .world-state, + .profile { + min-height: 280px; + } +} + +/* Mobile (Tablet/Phone) */ +@media (max-width: 768px) { + .container { + height: auto; + min-height: 100vh; + padding: 16px; + } + + header { + flex-direction: column; + gap: 16px; + padding-bottom: 16px; + margin-bottom: 16px; + align-items: flex-start; + } + + h1 { + font-size: 1.5rem; + } + + .stats { + width: 100%; + justify-content: space-between; + gap: 16px; + text-align: center; + } + + .stat-val { + font-size: 1.75rem; + } + + .stat-lbl { + font-size: .625rem; + } + + .controls { + flex-wrap: wrap; + gap: 8px; + padding: 10px 0; + margin-bottom: 16px; + } + + .spacer { + display: none; + } + + .chk-label { + width: 100%; + justify-content: center; + } + + .btn-group { + width: 100%; + display: flex; + gap: 6px; + } + + .btn-group .btn { + padding: 10px 8px; + font-size: .75rem; + } + + .btn-group .btn-icon { + padding: 10px 8px; + justify-content: center; + } + + .btn-group .btn-icon span { + display: none; + } + + main { + display: flex; + flex-direction: column; + gap: 16px; + } + + .left, + .right { + gap: 16px; + } + + .right { + display: flex; + flex-direction: column; + } + + .timeline { + max-height: 400px; + } + + .relations, + .profile { + min-height: 350px; + max-height: 350px; + height: 350px; + } + + #relation-chart { + height: 100%; + min-height: 300px; + } + + .world-state { + min-height: 180px; + max-height: 180px; + } + + .card { + padding: 16px; + } + + .keywords { + gap: 8px; + margin-top: 12px; + } + + .tag { + padding: 6px 14px; + font-size: .8125rem; + } + + .tl-item { + padding-left: 24px; + padding-bottom: 24px; + } + + .tl-title { + font-size: .9375rem; + } + + .tl-brief { + font-size: .8125rem; + line-height: 1.6; + } + + .modal-box { + max-width: 100%; + max-height: 100%; + height: 100%; + border: none; + border-radius: 0; + } + + .modal-head, + .modal-body, + .modal-foot { + padding: 16px; + } + + /* Settings Modal Mobile Fix */ + .settings-modal-box .modal-head { + flex-direction: column; + align-items: flex-start; + gap: 12px; + padding: 20px 16px 0; + } + + .settings-modal-box .modal-close { + position: absolute; + top: 12px; + right: 12px; + z-index: 20; + } + + .settings-tabs { + width: 100%; + margin-bottom: 0; + gap: 4px; + overflow-x: auto; + white-space: nowrap; + justify-content: flex-start; + padding-bottom: 0; + align-self: flex-start; + -ms-overflow-style: none; + scrollbar-width: none; + } + + .settings-tabs::-webkit-scrollbar { + display: none; + } + + .settings-tab { + padding: 8px 12px; + font-size: 0.75rem; + flex-shrink: 0; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: 0; + top: 0; + position: relative; + z-index: 10; + background: transparent; + } + + .settings-tab.active { + background: transparent; + border: none; + border-bottom: 2px solid var(--txt); + color: var(--txt); + padding-bottom: 6px; + } + + .settings-row { + flex-direction: column; + gap: 12px; + } + + .settings-field { + min-width: 100%; + } + + .settings-field input, + .settings-field select { + padding: 12px 14px; + font-size: 1rem; + } + + .fullscreen .modal-box { + width: 100%; + height: 100%; + border-radius: 0; + } + + .hf-step-content { + padding-left: 0; + margin-top: 12px; + } + + .hf-config-row { + flex-direction: column; + gap: 4px; + } + + .hf-config-label { + width: auto; + font-size: .75rem; + color: var(--txt3); + } + + .hf-intro-badges { + gap: 8px; + } + + .hf-badge { + font-size: .6875rem; + padding: 3px 10px; + } + + .facts-list { + max-height: 180px; + } + + .fact-item { + padding: 6px 8px; + font-size: 0.75rem; + } + + #recall-log-modal .modal-box { + max-width: 100%; + max-height: 100%; + height: 100%; + border-radius: 0; + } + + .debug-log-viewer, + #recall-log-content { + font-size: 11px; + padding: 12px; + line-height: 1.5; + } + + .world-item { + flex-direction: column; + gap: 4px; + padding: 8px; + } + + .vector-stats { + gap: 8px; + } + + .vector-stat-sep { + display: none; + } + + .vector-stat-col { + flex-direction: row; + gap: 4px; + } +} + +/* Small Mobile */ +@media (max-width: 480px) { + .container { + padding: 12px; + } + + header { + padding-bottom: 12px; + margin-bottom: 12px; + } + + .stats { + gap: 8px; + } + + h1 { + font-size: 1.25rem; + } + + .stat { + flex: 1; + } + + .stat-val { + font-size: 1.5rem; + } + + .controls { + gap: 6px; + padding: 8px 0; + margin-bottom: 12px; + } + + .btn-group .btn { + padding: 10px 6px; + font-size: .6875rem; + } + + main, + .left, + .right { + gap: 12px; + } + + .card { + padding: 12px; + } + + .sec-title { + font-size: .6875rem; + } + + .sec-btn { + font-size: .625rem; + padding: 3px 8px; + } + + .relations, + .profile { + min-height: 300px; + max-height: 300px; + height: 300px; + } + + #relation-chart { + height: 100%; + min-height: 250px; + } + + .world-state { + min-height: 150px; + max-height: 150px; + } + + .keywords { + gap: 6px; + margin-top: 10px; + } + + .tag { + padding: 5px 10px; + font-size: .75rem; + } + + .tl-item { + padding-left: 20px; + padding-bottom: 20px; + margin-left: 6px; + } + + .tl-dot { + width: 7px; + height: 7px; + left: -4px; + } + + .tl-head { + flex-direction: column; + align-items: flex-start; + gap: 2px; + } + + .tl-title { + font-size: .875rem; + } + + .tl-time { + font-size: .6875rem; + } + + .tl-brief { + font-size: .8rem; + margin-bottom: 8px; + } + + .tl-meta { + flex-direction: column; + gap: 4px; + font-size: .6875rem; + } + + .modal-head h2 { + font-size: .875rem; + } + + .settings-section-title { + font-size: .625rem; + } + + .settings-field label { + font-size: .6875rem; + } + + .settings-field-inline label { + font-size: .75rem; + } + + .settings-hint { + font-size: .6875rem; + } + + .btn-sm { + padding: 10px 14px; + font-size: .75rem; + width: 100%; + } + + .editor-ta { + min-height: 200px; + font-size: .75rem; + } + + .settings-tabs { + gap: 2px; + } + + .settings-tab { + padding: 8px 6px; + } + + .guide-container { + font-size: .75rem; + } + + .guide-title { + font-size: .875rem; + gap: 8px; + } + + .guide-num { + width: 22px; + height: 22px; + font-size: .6875rem; + } + + .guide-section { + margin-bottom: 16px; + padding-bottom: 14px; + } + + .guide-steps { + gap: 12px; + } + + .guide-step-title { + font-size: .8125rem; + } + + .guide-step-desc { + font-size: .75rem; + } + + .guide-card-title { + font-size: .75rem; + } + + .guide-card-desc { + font-size: .6875rem; + } + + .guide-highlight { + padding: 12px 14px; + } + + .guide-highlight-title { + font-size: .8125rem; + } + + .guide-list { + margin: 10px 0 16px; + font-size: .75rem; + } + + .guide-list li { + padding-left: 18px; + margin-bottom: 8px; + } + + .guide-list-inner li { + font-size: .75rem; + padding-left: 16px; + } + + .guide-tip { + padding: 8px 10px; + gap: 8px; + } + + .guide-tip-icon { + font-size: .875rem; + } + + .guide-tip-text { + font-size: .75rem; + } + + .guide-faq-q { + font-size: .75rem; + } + + .guide-faq-a { + font-size: .75rem; + } + + .guide-faq-item { + padding: 10px 0; + } +} + +@media (hover: none) and (pointer: coarse) { + .btn { + min-height: 44px; + } + + .tag { + min-height: 36px; + display: flex; + align-items: center; + } + + .tag:hover { + transform: none; + } + + .tl-item:hover .tl-dot { + transform: none; + } + + .modal-close { + width: 44px; + height: 44px; + } + + .settings-field input, + .settings-field select { + min-height: 44px; + } + + .settings-field-inline input[type="checkbox"] { + width: 22px; + height: 22px; + } + + .sec-btn { + min-height: 32px; + padding: 6px 12px; + } + + .guide-card:hover { + border-color: var(--bdr2); + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Compatibility with story-summary.html classes (neo-card → card mapping) + ═══════════════════════════════════════════════════════════════════════════ */ + +/* Neo-Brutalism header accent */ +h1 span { + color: var(--bg2); + background: var(--txt); + padding: 0 6px; +} + +/* Map .neo-card to the -a theme's .card style */ +.neo-card { + background: var(--bg2); + border: 2px solid var(--bdr); + box-shadow: var(--shadow); + padding: 24px; + border-radius: 4px; + margin-bottom: 1em; +} + +.neo-card:hover { + transform: translate(1px, 1px); + box-shadow: var(--shadow-hover); +} + +/* Map .neo-card-title to the -a theme's .sec-head + .sec-title style */ +.neo-card-title { + display: flex; + align-items: center; + gap: 1em; + font-size: 0.875rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--txt); + margin-bottom: 20px; + border-bottom: 2px solid var(--bdr); + padding-bottom: 8px; +} + +.neo-badge { + background: var(--txt); + color: var(--bg2); + padding: 4px 10px; + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.05em; + display: inline-block; + box-shadow: 1px 1px 0 var(--bdr); +} + +.neo-tools-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 32px; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 2px dashed var(--bdr); + color: var(--txt3); + font-weight: 800; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.1em; +} + +/* Specific tweaks for neo-card content in -a theme */ +.neo-card .settings-row { + margin-bottom: 12px; +} + +.neo-card .vector-stats { + background: var(--bg3); + border: 1px solid var(--bdr); + padding: 10px; + border-radius: 0; +} + +.neo-card .settings-hint { + color: var(--txt2); +} diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js new file mode 100644 index 0000000..7cff296 --- /dev/null +++ b/modules/story-summary/story-summary-ui.js @@ -0,0 +1,1720 @@ +// story-summary-ui.js +// iframe 内 UI 逻辑 + +(function () { + 'use strict'; + + // ═══════════════════════════════════════════════════════════════════════════ + // DOM Helpers + // ═══════════════════════════════════════════════════════════════════════════ + + const $ = id => document.getElementById(id); + const $$ = sel => document.querySelectorAll(sel); + const h = v => String(v ?? '').replace(/[&<>"']/g, c => + ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] + ); + const setHtml = (el, html) => { + if (!el) return; + const range = document.createRange(); + range.selectNodeContents(el); + // eslint-disable-next-line no-unsanitized/method + const fragment = range.createContextualFragment(String(html ?? '')); + el.replaceChildren(fragment); + }; + const setSelectOptions = (select, items, placeholderText) => { + if (!select) return; + select.replaceChildren(); + if (placeholderText != null) { + const option = document.createElement('option'); + option.value = ''; + option.textContent = placeholderText; + select.appendChild(option); + } + (items || []).forEach(item => { + const option = document.createElement('option'); + option.value = item; + option.textContent = item; + select.appendChild(option); + }); + }; + + // ═══════════════════════════════════════════════════════════════════════════ + // Constants + // ═══════════════════════════════════════════════════════════════════════════ + + const PARENT_ORIGIN = (() => { + try { return new URL(document.referrer).origin; } + catch { return window.location.origin; } + })(); + + const PROVIDER_DEFAULTS = { + st: { url: '', needKey: false, canFetch: false, needManualModel: false }, + openai: { url: 'https://api.openai.com', needKey: true, canFetch: true, needManualModel: false }, + google: { url: 'https://generativelanguage.googleapis.com', needKey: true, canFetch: false, needManualModel: true }, + claude: { url: 'https://api.anthropic.com', needKey: true, canFetch: false, needManualModel: true }, + custom: { url: '', needKey: true, canFetch: true, needManualModel: false } + }; + + const SECTION_META = { + keywords: { title: '编辑关键词', hint: '每行一个关键词,格式:关键词|权重(核心/重要/一般)' }, + events: { title: '编辑事件时间线', hint: '编辑时,每个事件要素都应完整' }, + characters: { title: '编辑人物关系', hint: '编辑时,每个要素都应完整' }, + arcs: { title: '编辑角色弧光', hint: '编辑时,每个要素都应完整' }, + facts: { title: '编辑事实图谱', hint: '每行一条:主体|谓词|值|趋势(可选)。删除用:主体|谓词|(留空值)' } + }; + + const TREND_COLORS = { + '破裂': '#444444', '厌恶': '#8b0000', '反感': '#cd5c5c', + '陌生': '#888888', '投缘': '#4a9a7e', '亲密': '#d87a7a', '交融': '#c71585' + }; + + const TREND_CLASS = { + '破裂': 'trend-broken', '厌恶': 'trend-hate', '反感': 'trend-dislike', + '陌生': 'trend-stranger', '投缘': 'trend-click', '亲密': 'trend-close', '交融': 'trend-merge' + }; + + const DEFAULT_FILTER_RULES = [ + { start: '', end: '' }, + { start: '', end: '' }, + { start: '```', end: '```' }, + ]; + + // ═══════════════════════════════════════════════════════════════════════════ + // State + // ═══════════════════════════════════════════════════════════════════════════ + + const config = { + api: { provider: 'st', url: '', key: '', model: '', modelCache: [] }, + gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null }, + trigger: { enabled: false, interval: 20, timing: 'before_user', role: 'system', useStream: true, maxPerRun: 100, wrapperHead: '', wrapperTail: '', forceInsertAtEnd: false }, + vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } } + }; + + let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] }; + let localGenerating = false; + let vectorGenerating = false; + let anchorGenerating = false; + let relationChart = null; + let relationChartFullscreen = null; + let currentEditSection = null; + let currentCharacterId = null; + let allNodes = []; + let allLinks = []; + let activeRelationTooltip = null; + let lastRecallLogText = ''; + + // ═══════════════════════════════════════════════════════════════════════════ + // Messaging + // ═══════════════════════════════════════════════════════════════════════════ + + function postMsg(type, data = {}) { + window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type, ...data }, PARENT_ORIGIN); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Config Management + // ═══════════════════════════════════════════════════════════════════════════ + + function loadConfig() { + try { + const s = localStorage.getItem('summary_panel_config'); + if (s) { + const p = JSON.parse(s); + Object.assign(config.api, p.api || {}); + Object.assign(config.gen, p.gen || {}); + Object.assign(config.trigger, p.trigger || {}); + if (p.vector) config.vector = p.vector; + if (config.trigger.timing === 'manual' && config.trigger.enabled) { + config.trigger.enabled = false; + saveConfig(); + } + } + } catch { } + } + + function applyConfig(cfg) { + if (!cfg) return; + Object.assign(config.api, cfg.api || {}); + Object.assign(config.gen, cfg.gen || {}); + Object.assign(config.trigger, cfg.trigger || {}); + if (cfg.vector) config.vector = cfg.vector; + if (config.trigger.timing === 'manual') config.trigger.enabled = false; + localStorage.setItem('summary_panel_config', JSON.stringify(config)); + } + + function saveConfig() { + try { + const settingsOpen = $('settings-modal')?.classList.contains('active'); + if (settingsOpen) config.vector = getVectorConfig(); + if (!config.vector) { + config.vector = { enabled: false, engine: 'online', online: { provider: 'siliconflow', key: '', model: 'BAAI/bge-m3' } }; + } + localStorage.setItem('summary_panel_config', JSON.stringify(config)); + postMsg('SAVE_PANEL_CONFIG', { config }); + } catch (e) { + console.error('saveConfig error:', e); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Vector Config UI + // ═══════════════════════════════════════════════════════════════════════════ + + function getVectorConfig() { + return { + enabled: $('vector-enabled')?.checked || false, + engine: 'online', + online: { + provider: 'siliconflow', + key: $('vector-api-key')?.value?.trim() || '', + model: 'BAAI/bge-m3', + }, + textFilterRules: collectFilterRules(), + }; + } + + function loadVectorConfig(cfg) { + if (!cfg) return; + $('vector-enabled').checked = !!cfg.enabled; + $('vector-config-area').classList.toggle('hidden', !cfg.enabled); + + if (cfg.online?.key) { + $('vector-api-key').value = cfg.online.key; + } + + renderFilterRules(cfg?.textFilterRules || DEFAULT_FILTER_RULES); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Filter Rules UI + // ═══════════════════════════════════════════════════════════════════════════ + + function renderFilterRules(rules) { + const list = $('filter-rules-list'); + if (!list) return; + + const items = rules?.length ? rules : []; + + setHtml(list, items.map((r, i) => ` +
+
+ + + +
+ +
+ `).join('')); + + // 绑定删除 + list.querySelectorAll('.btn-del-rule').forEach(btn => { + btn.onclick = () => { + btn.closest('.filter-rule-item')?.remove(); + updateFilterRulesCount(); + }; + }); + + updateFilterRulesCount(); + } + + function collectFilterRules() { + const list = $('filter-rules-list'); + if (!list) return []; + + const rules = []; + list.querySelectorAll('.filter-rule-item').forEach(item => { + const start = item.querySelector('.filter-rule-start')?.value?.trim() || ''; + const end = item.querySelector('.filter-rule-end')?.value?.trim() || ''; + if (start || end) { + rules.push({ start, end }); + } + }); + return rules; + } + + function addFilterRule() { + const list = $('filter-rules-list'); + if (!list) return; + + const idx = list.querySelectorAll('.filter-rule-item').length; + const div = document.createElement('div'); + div.className = 'filter-rule-item'; + div.dataset.idx = idx; + setHtml(div, ` +
+ + + +
+ + `); + div.querySelector('.btn-del-rule').onclick = () => { + div.remove(); + updateFilterRulesCount(); + }; + list.appendChild(div); + updateFilterRulesCount(); + } + + function updateFilterRulesCount() { + const el = $('filter-rules-count'); + if (!el) return; + const count = $('filter-rules-list')?.querySelectorAll('.filter-rule-item')?.length || 0; + el.textContent = count; + } + + + function updateOnlineStatus(status, message) { + const dot = $('online-api-status').querySelector('.status-dot'); + const text = $('online-api-status').querySelector('.status-text'); + dot.className = 'status-dot ' + status; + text.textContent = message; + } + + function updateVectorStats(stats) { + $('vector-atom-count').textContent = stats.stateVectors || 0; + $('vector-chunk-count').textContent = stats.chunkCount || 0; + $('vector-event-count').textContent = stats.eventVectors || 0; + } + + function showVectorMismatchWarning(show) { + $('vector-mismatch-warning').classList.toggle('hidden', !show); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // 记忆锚点(L0)UI + // ═══════════════════════════════════════════════════════════════════════════ + + function updateAnchorStats(stats) { + const extracted = stats.extracted || 0; + const total = stats.total || 0; + const pending = stats.pending || 0; + const empty = stats.empty || 0; + const fail = stats.fail || 0; + const atomsCount = stats.atomsCount || 0; + + $('anchor-extracted').textContent = extracted; + $('anchor-total').textContent = total; + $('anchor-pending').textContent = pending; + $('anchor-atoms-count').textContent = atomsCount; + + const pendingWrap = $('anchor-pending-wrap'); + if (pendingWrap) { + pendingWrap.classList.toggle('hidden', pending === 0); + } + + // 显示 empty/fail 信息 + const extraWrap = $('anchor-extra-wrap'); + const extraSep = $('anchor-extra-sep'); + const extra = $('anchor-extra'); + if (extraWrap && extra) { + if (empty > 0 || fail > 0) { + const parts = []; + if (empty > 0) parts.push(`空 ${empty}`); + if (fail > 0) parts.push(`失败 ${fail}`); + extra.textContent = parts.join(' · '); + extraWrap.style.display = ''; + if (extraSep) extraSep.style.display = ''; + } else { + extraWrap.style.display = 'none'; + if (extraSep) extraSep.style.display = 'none'; + } + } + + const emptyWarning = $('vector-empty-l0-warning'); + if (emptyWarning) { + emptyWarning.classList.toggle('hidden', extracted > 0); + } + } + + function updateAnchorProgress(current, total, message) { + const progress = $('anchor-progress'); + const btnGen = $('btn-anchor-generate'); + const btnClear = $('btn-anchor-clear'); + const btnCancel = $('btn-anchor-cancel'); + + if (current < 0) { + progress.classList.add('hidden'); + btnGen.classList.remove('hidden'); + btnClear.classList.remove('hidden'); + btnCancel.classList.add('hidden'); + anchorGenerating = false; + } else { + anchorGenerating = true; + progress.classList.remove('hidden'); + btnGen.classList.add('hidden'); + btnClear.classList.add('hidden'); + btnCancel.classList.remove('hidden'); + + const percent = total > 0 ? Math.round(current / total * 100) : 0; + progress.querySelector('.progress-inner').style.width = percent + '%'; + progress.querySelector('.progress-text').textContent = message || `${current}/${total}`; + } + } + + function initAnchorUI() { + $('btn-anchor-generate').onclick = () => { + if (anchorGenerating) return; + postMsg('ANCHOR_GENERATE'); + }; + + $('btn-anchor-clear').onclick = () => { + if (confirm('清空所有记忆锚点?(L0 向量也会一并清除)')) { + postMsg('ANCHOR_CLEAR'); + } + }; + + $('btn-anchor-cancel').onclick = () => { + postMsg('ANCHOR_CANCEL'); + }; + } + + function initVectorUI() { + $('vector-enabled').onchange = e => { + $('vector-config-area').classList.toggle('hidden', !e.target.checked); + }; + + $('btn-test-vector-api').onclick = () => { + postMsg('VECTOR_TEST_ONLINE', { + provider: 'siliconflow', + config: { + key: $('vector-api-key').value.trim(), + model: 'BAAI/bge-m3', + } + }); + }; + + $('btn-add-filter-rule').onclick = addFilterRule; + + $('btn-gen-vectors').onclick = () => { + if (vectorGenerating) return; + postMsg('VECTOR_GENERATE', { config: getVectorConfig() }); + }; + + $('btn-clear-vectors').onclick = () => { + if (confirm('确定清空所有向量数据?')) postMsg('VECTOR_CLEAR'); + }; + + $('btn-cancel-vectors').onclick = () => postMsg('VECTOR_CANCEL_GENERATE'); + + $('btn-export-vectors').onclick = () => { + $('btn-export-vectors').disabled = true; + $('vector-io-status').textContent = '导出中...'; + postMsg('VECTOR_EXPORT'); + }; + + $('btn-import-vectors').onclick = () => { + $('btn-import-vectors').disabled = true; + $('vector-io-status').textContent = '导入中...'; + postMsg('VECTOR_IMPORT_PICK'); + }; + + initAnchorUI(); + postMsg('REQUEST_ANCHOR_STATS'); + } + // ═══════════════════════════════════════════════════════════════════════════ + // Settings Modal + // ═══════════════════════════════════════════════════════════════════════════ + + function updateProviderUI(provider) { + const pv = PROVIDER_DEFAULTS[provider] || PROVIDER_DEFAULTS.custom; + const isSt = provider === 'st'; + + $('api-url-row').classList.toggle('hidden', isSt); + $('api-key-row').classList.toggle('hidden', !pv.needKey); + $('api-model-manual-row').classList.toggle('hidden', isSt || !pv.needManualModel); + $('api-model-select-row').classList.toggle('hidden', isSt || pv.needManualModel || !config.api.modelCache.length); + $('api-connect-row').classList.toggle('hidden', isSt || !pv.canFetch); + + const urlInput = $('api-url'); + if (!urlInput.value && pv.url) urlInput.value = pv.url; + } + + function openSettings() { + $('api-provider').value = config.api.provider; + $('api-url').value = config.api.url; + $('api-key').value = config.api.key; + $('api-model-text').value = config.api.model; + $('gen-temp').value = config.gen.temperature ?? ''; + $('gen-top-p').value = config.gen.top_p ?? ''; + $('gen-top-k').value = config.gen.top_k ?? ''; + $('gen-presence').value = config.gen.presence_penalty ?? ''; + $('gen-frequency').value = config.gen.frequency_penalty ?? ''; + $('trigger-enabled').checked = config.trigger.enabled; + $('trigger-interval').value = config.trigger.interval; + $('trigger-timing').value = config.trigger.timing; + $('trigger-role').value = config.trigger.role || 'system'; + $('trigger-stream').checked = config.trigger.useStream !== false; + $('trigger-max-per-run').value = config.trigger.maxPerRun || 100; + $('trigger-wrapper-head').value = config.trigger.wrapperHead || ''; + $('trigger-wrapper-tail').value = config.trigger.wrapperTail || ''; + $('trigger-insert-at-end').checked = !!config.trigger.forceInsertAtEnd; + + const en = $('trigger-enabled'); + if (config.trigger.timing === 'manual') { + en.checked = false; + en.disabled = true; + en.parentElement.style.opacity = '.5'; + } else { + en.disabled = false; + en.parentElement.style.opacity = '1'; + } + + if (config.api.modelCache.length) { + setHtml($('api-model-select'), config.api.modelCache.map(m => + `` + ).join('')); + } + + updateProviderUI(config.api.provider); + if (config.vector) loadVectorConfig(config.vector); + + // Initialize sub-options visibility + const autoSummaryOptions = $('auto-summary-options'); + if (autoSummaryOptions) { + autoSummaryOptions.classList.toggle('hidden', !config.trigger.enabled); + } + const insertWrapperOptions = $('insert-wrapper-options'); + if (insertWrapperOptions) { + insertWrapperOptions.classList.toggle('hidden', !config.trigger.forceInsertAtEnd); + } + + $('settings-modal').classList.add('active'); + + // Default to first tab + $$('.settings-tab').forEach(t => t.classList.remove('active')); + $$('.settings-tab[data-tab="tab-summary"]').forEach(t => t.classList.add('active')); + $$('.tab-pane').forEach(p => p.classList.remove('active')); + $('tab-summary').classList.add('active'); + + postMsg('SETTINGS_OPENED'); + } + + function closeSettings(save) { + if (save) { + const pn = id => { const v = $(id).value; return v === '' ? null : parseFloat(v); }; + const provider = $('api-provider').value; + const pv = PROVIDER_DEFAULTS[provider] || PROVIDER_DEFAULTS.custom; + + config.api.provider = provider; + config.api.url = $('api-url').value; + config.api.key = $('api-key').value; + config.api.model = provider === 'st' ? '' : pv.needManualModel ? $('api-model-text').value : $('api-model-select').value; + + config.gen.temperature = pn('gen-temp'); + config.gen.top_p = pn('gen-top-p'); + config.gen.top_k = pn('gen-top-k'); + config.gen.presence_penalty = pn('gen-presence'); + config.gen.frequency_penalty = pn('gen-frequency'); + + const timing = $('trigger-timing').value; + config.trigger.timing = timing; + config.trigger.role = $('trigger-role').value || 'system'; + config.trigger.enabled = timing === 'manual' ? false : $('trigger-enabled').checked; + config.trigger.interval = Math.max(1, Math.min(30, parseInt($('trigger-interval').value) || 20)); + config.trigger.useStream = $('trigger-stream').checked; + config.trigger.maxPerRun = parseInt($('trigger-max-per-run').value) || 100; + config.trigger.wrapperHead = $('trigger-wrapper-head').value; + config.trigger.wrapperTail = $('trigger-wrapper-tail').value; + config.trigger.forceInsertAtEnd = $('trigger-insert-at-end').checked; + + config.vector = getVectorConfig(); + saveConfig(); + } + + $('settings-modal').classList.remove('active'); + postMsg('SETTINGS_CLOSED'); + } + + async function fetchModels() { + const btn = $('btn-connect'); + const provider = $('api-provider').value; + + if (!PROVIDER_DEFAULTS[provider]?.canFetch) { + alert('当前渠道不支持自动拉取模型'); + return; + } + + let baseUrl = $('api-url').value.trim().replace(/\/+$/, ''); + const apiKey = $('api-key').value.trim(); + + if (!apiKey) { + alert('请先填写 API KEY'); + return; + } + + btn.disabled = true; + btn.textContent = '连接中...'; + + try { + const tryFetch = async url => { + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' } + }); + return res.ok ? (await res.json())?.data?.map(m => m?.id).filter(Boolean) || null : null; + }; + + if (baseUrl.endsWith('/v1')) baseUrl = baseUrl.slice(0, -3); + + let models = await tryFetch(`${baseUrl}/v1/models`); + if (!models) models = await tryFetch(`${baseUrl}/models`); + if (!models?.length) throw new Error('未获取到模型列表'); + + config.api.modelCache = [...new Set(models)]; + const sel = $('api-model-select'); + setSelectOptions(sel, config.api.modelCache); + $('api-model-select-row').classList.remove('hidden'); + + if (!config.api.model && models.length) { + config.api.model = models[0]; + sel.value = models[0]; + } else if (config.api.model) { + sel.value = config.api.model; + } + + saveConfig(); + alert(`成功获取 ${models.length} 个模型`); + } catch (e) { + alert('连接失败:' + (e.message || '请检查 URL 和 KEY')); + } finally { + btn.disabled = false; + btn.textContent = '连接 / 拉取模型列表'; + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Rendering Functions + // ═══════════════════════════════════════════════════════════════════════════ + + function renderKeywords(kw) { + summaryData.keywords = kw || []; + const wc = { '核心': 'p', '重要': 's', high: 'p', medium: 's' }; + setHtml($('keywords-cloud'), kw.length + ? kw.map(k => `${h(k.text)}`).join('') + : '
暂无关键词
'); + } + + function renderTimeline(ev) { + summaryData.events = ev || []; + const c = $('timeline-list'); + if (!ev?.length) { + setHtml(c, '
暂无事件记录
'); + return; + } + setHtml(c, ev.map(e => { + const participants = (e.participants || e.characters || []).map(h).join('、'); + return `
+
+
+
${h(e.title || '')}
+
${h(e.timeLabel || '')}
+
+
${h(e.summary || e.brief || '')}
+
+ 人物:${participants || '—'} + ${h(e.type || '')}${e.type && e.weight ? ' · ' : ''}${h(e.weight || '')} +
+
`; + }).join('')); + } + + function getCharName(c) { + return typeof c === 'string' ? c : c.name; + } + + function hideRelationTooltip() { + if (activeRelationTooltip) { + activeRelationTooltip.remove(); + activeRelationTooltip = null; + } + } + + function showRelationTooltip(from, to, fromLabel, toLabel, fromTrend, toTrend, x, y, container) { + hideRelationTooltip(); + const tip = document.createElement('div'); + const mobile = innerWidth <= 768; + const fc = TREND_COLORS[fromTrend] || '#888'; + const tc = TREND_COLORS[toTrend] || '#888'; + + setHtml(tip, `
+ ${fromLabel ? `
${h(from)}→${h(to)}: ${h(fromLabel)} [${h(fromTrend)}]
` : ''} + ${toLabel ? `
${h(to)}→${h(from)}: ${h(toLabel)} [${h(toTrend)}]
` : ''} +
`); + + tip.style.cssText = mobile + ? 'position:absolute;left:8px;bottom:8px;background:#fff;color:#333;padding:10px 14px;border:1px solid #ddd;border-radius:6px;font-size:12px;z-index:100;box-shadow:0 2px 12px rgba(0,0,0,.15);max-width:calc(100% - 16px)' + : `position:absolute;left:${Math.max(80, Math.min(x, container.clientWidth - 80))}px;top:${Math.max(60, y)}px;transform:translate(-50%,-100%);background:#fff;color:#333;padding:10px 16px;border:1px solid #ddd;border-radius:6px;font-size:12px;z-index:1000;box-shadow:0 4px 12px rgba(0,0,0,.15);max-width:280px`; + + container.style.position = 'relative'; + container.appendChild(tip); + activeRelationTooltip = tip; + } + + function renderRelations(data) { + summaryData.characters = data || { main: [], relationships: [] }; + const dom = $('relation-chart'); + if (!relationChart) relationChart = echarts.init(dom); + + const rels = data?.relationships || []; + const allNames = new Set((data?.main || []).map(getCharName)); + rels.forEach(r => { if (r.from) allNames.add(r.from); if (r.to) allNames.add(r.to); }); + + const degrees = {}; + rels.forEach(r => { + degrees[r.from] = (degrees[r.from] || 0) + 1; + degrees[r.to] = (degrees[r.to] || 0) + 1; + }); + + const nodeColors = { main: '#d87a7a', sec: '#f1c3c3', ter: '#888888', qua: '#b8b8b8' }; + const sortedDegs = Object.values(degrees).sort((a, b) => b - a); + const getPercentile = deg => { + if (!sortedDegs.length || deg === 0) return 100; + const rank = sortedDegs.filter(d => d > deg).length; + return (rank / sortedDegs.length) * 100; + }; + + allNodes = Array.from(allNames).map(name => { + const deg = degrees[name] || 0; + const pct = getPercentile(deg); + let col, fontWeight; + if (pct < 30) { col = nodeColors.main; fontWeight = '600'; } + else if (pct < 60) { col = nodeColors.sec; fontWeight = '500'; } + else if (pct < 90) { col = nodeColors.ter; fontWeight = '400'; } + else { col = nodeColors.qua; fontWeight = '400'; } + return { + id: name, name, symbol: 'circle', + symbolSize: Math.min(36, Math.max(16, deg * 3 + 12)), + draggable: true, + itemStyle: { color: col, borderColor: '#fff', borderWidth: 2, shadowColor: 'rgba(0,0,0,.1)', shadowBlur: 6, shadowOffsetY: 2 }, + label: { show: true, position: 'right', distance: 5, color: '#333', fontSize: 11, fontWeight }, + degree: deg + }; + }); + + const relMap = new Map(); + rels.forEach(r => { + const k = [r.from, r.to].sort().join('|||'); + if (!relMap.has(k)) relMap.set(k, { from: r.from, to: r.to, fromLabel: '', toLabel: '', fromTrend: '', toTrend: '' }); + const e = relMap.get(k); + if (r.from === e.from) { e.fromLabel = r.label || r.type || ''; e.fromTrend = r.trend || ''; } + else { e.toLabel = r.label || r.type || ''; e.toTrend = r.trend || ''; } + }); + + allLinks = Array.from(relMap.values()).map(r => { + const fc = TREND_COLORS[r.fromTrend] || '#b8b8b8'; + const tc = TREND_COLORS[r.toTrend] || '#b8b8b8'; + return { + source: r.from, target: r.to, fromName: r.from, toName: r.to, + fromLabel: r.fromLabel, toLabel: r.toLabel, fromTrend: r.fromTrend, toTrend: r.toTrend, + lineStyle: { width: 1, color: '#d8d8d8', curveness: 0, opacity: 1 }, + label: { + show: true, position: 'middle', distance: 0, + formatter: '{a|◀}{b|▶}', + rich: { a: { color: fc, fontSize: 10 }, b: { color: tc, fontSize: 10 } }, + align: 'center', verticalAlign: 'middle', offset: [0, -0.1] + }, + emphasis: { lineStyle: { width: 1.5, color: '#aaa' }, label: { fontSize: 11 } } + }; + }); + + if (!allNodes.length) { relationChart.clear(); return; } + + const updateChart = (nodes, links, focusId = null) => { + const fadeOpacity = 0.2; + const processedNodes = focusId ? nodes.map(n => { + const rl = links.filter(l => l.source === focusId || l.target === focusId); + const rn = new Set([focusId]); + rl.forEach(l => { rn.add(l.source); rn.add(l.target); }); + const isRelated = rn.has(n.id); + return { ...n, itemStyle: { ...n.itemStyle, opacity: isRelated ? 1 : fadeOpacity }, label: { ...n.label, opacity: isRelated ? 1 : fadeOpacity } }; + }) : nodes; + + const processedLinks = focusId ? links.map(l => { + const isRelated = l.source === focusId || l.target === focusId; + return { ...l, lineStyle: { ...l.lineStyle, opacity: isRelated ? 1 : fadeOpacity }, label: { ...l.label, opacity: isRelated ? 1 : fadeOpacity } }; + }) : links; + + relationChart.setOption({ + backgroundColor: 'transparent', + tooltip: { show: false }, + hoverLayerThreshold: Infinity, + series: [{ + type: 'graph', layout: 'force', roam: true, draggable: true, + animation: true, animationDuration: 800, animationDurationUpdate: 300, animationEasingUpdate: 'cubicInOut', + progressive: 0, hoverAnimation: false, + data: processedNodes, links: processedLinks, + force: { initLayout: 'circular', repulsion: 350, edgeLength: [80, 160], gravity: .12, friction: .6, layoutAnimation: true }, + label: { show: true }, edgeLabel: { show: true, position: 'middle' }, + emphasis: { disabled: true } + }] + }); + }; + + updateChart(allNodes, allLinks); + setTimeout(() => relationChart.resize(), 0); + + relationChart.off('click'); + relationChart.on('click', p => { + if (p.dataType === 'node') { + hideRelationTooltip(); + const id = p.data.id; + selectCharacter(id); + updateChart(allNodes, allLinks, id); + } else if (p.dataType === 'edge') { + const d = p.data; + const e = p.event?.event; + if (e) { + const rect = dom.getBoundingClientRect(); + showRelationTooltip(d.fromName, d.toName, d.fromLabel, d.toLabel, d.fromTrend, d.toTrend, + e.offsetX || (e.clientX - rect.left), e.offsetY || (e.clientY - rect.top), dom); + } + } + }); + + relationChart.getZr().on('click', p => { + if (!p.target) { + hideRelationTooltip(); + updateChart(allNodes, allLinks); + } + }); + } + + function selectCharacter(id) { + currentCharacterId = id; + const txt = $('sel-char-text'); + const opts = $('char-sel-opts'); + if (opts && id) { + opts.querySelectorAll('.sel-opt').forEach(o => { + if (o.dataset.value === id) { + o.classList.add('sel'); + if (txt) txt.textContent = o.textContent; + } else { + o.classList.remove('sel'); + } + }); + } else if (!id && txt) { + txt.textContent = '选择角色'; + } + renderCharacterProfile(); + if (relationChart && id) { + const opt = relationChart.getOption(); + const idx = opt?.series?.[0]?.data?.findIndex(n => n.id === id || n.name === id); + if (idx >= 0) relationChart.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: idx }); + } + } + + function updateCharacterSelector(arcs) { + const opts = $('char-sel-opts'); + const txt = $('sel-char-text'); + if (!opts) return; + if (!arcs?.length) { + setHtml(opts, '
暂无角色
'); + if (txt) txt.textContent = '暂无角色'; + currentCharacterId = null; + return; + } + setHtml(opts, arcs.map(a => `
${h(a.name || '角色')}
`).join('')); + opts.querySelectorAll('.sel-opt').forEach(o => { + o.onclick = e => { + e.stopPropagation(); + if (o.dataset.value) { + selectCharacter(o.dataset.value); + $('char-sel').classList.remove('open'); + } + }; + }); + if (currentCharacterId && arcs.some(a => (a.id || a.name) === currentCharacterId)) { + selectCharacter(currentCharacterId); + } else if (arcs.length) { + selectCharacter(arcs[0].id || arcs[0].name); + } + } + + function renderCharacterProfile() { + const c = $('profile-content'); + const arcs = summaryData.arcs || []; + const rels = summaryData.characters?.relationships || []; + + if (!currentCharacterId || !arcs.length) { + setHtml(c, '
暂无角色数据
'); + return; + } + + const arc = arcs.find(a => (a.id || a.name) === currentCharacterId); + if (!arc) { + setHtml(c, '
未找到角色数据
'); + return; + } + + const name = arc.name || '角色'; + const moments = (arc.moments || arc.beats || []).map(m => typeof m === 'string' ? m : m.text); + const outRels = rels.filter(r => r.from === name); + const inRels = rels.filter(r => r.to === name); + + setHtml(c, ` +
+
+
${h(name)}
+
${h(arc.trajectory || arc.phase || '')}
+
+
+
+ 弧光进度 + ${Math.round((arc.progress || 0) * 100)}% +
+
+
+
+
+ ${moments.length ? ` +
+
关键时刻
+ ${moments.map(m => `
${h(m)}
`).join('')} +
+ ` : ''} +
+
+
+
${h(name)}对别人的羁绊:
+ ${outRels.length ? outRels.map(r => ` +
+ 对${h(r.to)}: + ${h(r.label || '—')} + ${r.trend ? `${h(r.trend)}` : ''} +
+ `).join('') : '
暂无关系记录
'} +
+
+
别人对${h(name)}的羁绊:
+ ${inRels.length ? inRels.map(r => ` +
+ ${h(r.from)}: + ${h(r.label || '—')} + ${r.trend ? `${h(r.trend)}` : ''} +
+ `).join('') : '
暂无关系记录
'} +
+
+ `); + } + + function renderArcs(arcs) { + summaryData.arcs = arcs || []; + updateCharacterSelector(arcs || []); + renderCharacterProfile(); + } + + function updateStats(s) { + if (!s) return; + $('stat-summarized').textContent = s.summarizedUpTo ?? 0; + $('stat-events').textContent = s.eventsCount ?? 0; + const p = s.pendingFloors ?? 0; + $('stat-pending').textContent = p; + $('pending-warning').classList.toggle('hidden', p !== -1); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Modals + // ═══════════════════════════════════════════════════════════════════════════ + + function openRelationsFullscreen() { + $('rel-fs-modal').classList.add('active'); + const dom = $('relation-chart-fullscreen'); + if (!relationChartFullscreen) relationChartFullscreen = echarts.init(dom); + + if (!allNodes.length) { + relationChartFullscreen.clear(); + return; + } + + relationChartFullscreen.setOption({ + tooltip: { show: false }, + hoverLayerThreshold: Infinity, + series: [{ + type: 'graph', layout: 'force', roam: true, draggable: true, + animation: true, animationDuration: 800, animationDurationUpdate: 300, animationEasingUpdate: 'cubicInOut', + progressive: 0, hoverAnimation: false, + data: allNodes.map(n => ({ + ...n, + symbolSize: Array.isArray(n.symbolSize) ? [n.symbolSize[0] * 1.3, n.symbolSize[1] * 1.3] : n.symbolSize * 1.3, + label: { ...n.label, fontSize: 14 } + })), + links: allLinks.map(l => ({ ...l, label: { ...l.label, fontSize: 18 } })), + force: { repulsion: 700, edgeLength: [150, 280], gravity: .06, friction: .6, layoutAnimation: true }, + label: { show: true }, edgeLabel: { show: true, position: 'middle' }, + emphasis: { disabled: true } + }] + }); + + setTimeout(() => relationChartFullscreen.resize(), 100); + postMsg('FULLSCREEN_OPENED'); + } + + function closeRelationsFullscreen() { + $('rel-fs-modal').classList.remove('active'); + postMsg('FULLSCREEN_CLOSED'); + } + + function renderArcsEditor(arcs) { + const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }]; + const es = $('editor-struct'); + + setHtml(es, ` +
+ ${list.map((a, i) => ` +
+
+
+
+ +
+
+
角色弧光 ${i + 1}
+
+ `).join('')} +
+
+ `); + + es.querySelectorAll('.arc-item').forEach(addDeleteHandler); + + $('arc-add').onclick = () => { + const listEl = $('arc-list'); + const idx = listEl.querySelectorAll('.arc-item').length; + const div = document.createElement('div'); + div.className = 'struct-item arc-item'; + div.dataset.index = idx; + setHtml(div, ` +
+
+
+ +
+
+
角色弧光 ${idx + 1}
+ `); + addDeleteHandler(div); + listEl.appendChild(div); + }; + } + + + function setRecallLog(text) { + lastRecallLogText = text || ''; + updateRecallLogDisplay(); + } + + function updateRecallLogDisplay() { + const content = $('recall-log-content'); + if (!content) return; + + if (lastRecallLogText) { + content.textContent = lastRecallLogText; + content.classList.remove('recall-empty'); + } else { + setHtml(content, `
+ 暂无召回日志

+ 当 AI 生成回复时,系统会自动进行记忆召回。

+ 召回日志将显示:
+ • [L0] Query Understanding - 意图识别
+ • [L1] Constraints - 硬约束注入
+ • [L2] Narrative Retrieval - 事件召回
+ • [L3] Evidence Assembly - 证据装配
+ • [L4] Prompt Formatting - 格式化
+ • [Budget] Token 预算使用情况
+ • [Quality] 质量指标与潜在问题 +
`); + } + } + + + // ═══════════════════════════════════════════════════════════════════════════ + // Editor + // ═══════════════════════════════════════════════════════════════════════════ + + function preserveAddedAt(n, o) { + if (o?._addedAt != null) n._addedAt = o._addedAt; + return n; + } + + function createDelBtn() { + const b = document.createElement('button'); + b.type = 'button'; + b.className = 'btn btn-sm btn-del'; + b.textContent = '删除'; + return b; + } + + function addDeleteHandler(item) { + const del = createDelBtn(); + (item.querySelector('.struct-actions') || item).appendChild(del); + del.onclick = () => item.remove(); + } + + function renderEventsEditor(events) { + const list = events?.length ? events : [{ id: 'evt-1', title: '', timeLabel: '', summary: '', participants: [], type: '日常', weight: '点睛' }]; + let maxId = 0; + list.forEach(e => { + const m = e.id?.match(/evt-(\d+)/); + if (m) maxId = Math.max(maxId, +m[1]); + }); + + const es = $('editor-struct'); + setHtml(es, list.map(ev => { + const id = ev.id || `evt-${++maxId}`; + return `
+
+ + +
+
+ +
+
+ +
+
+ + +
+
ID:${h(id)}
+
`; + }).join('') + '
'); + + es.querySelectorAll('.event-item').forEach(addDeleteHandler); + + $('event-add').onclick = () => { + let nmax = maxId; + es.querySelectorAll('.event-item').forEach(it => { + const m = it.dataset.id?.match(/evt-(\d+)/); + if (m) nmax = Math.max(nmax, +m[1]); + }); + const nid = `evt-${nmax + 1}`; + const div = document.createElement('div'); + div.className = 'struct-item event-item'; + div.dataset.id = nid; + setHtml(div, ` +
+
+
+
+ + +
+
ID:${h(nid)}
+ `); + addDeleteHandler(div); + es.insertBefore(div, $('event-add').parentElement); + }; + } + + function renderCharactersEditor(data) { + const d = data || { main: [], relationships: [] }; + const main = (d.main || []).map(getCharName); + const rels = d.relationships || []; + const trendOpts = ['破裂', '厌恶', '反感', '陌生', '投缘', '亲密', '交融']; + + const es = $('editor-struct'); + setHtml(es, ` +
+
角色列表
+
+ ${(main.length ? main : ['']).map(n => `
`).join('')} +
+
+
+
+
人物关系
+
+ ${(rels.length ? rels : [{ from: '', to: '', label: '', trend: '陌生' }]).map(r => ` +
+ + + + +
+ `).join('')} +
+
+
+ `); + + es.querySelectorAll('.char-main-item,.char-rel-item').forEach(addDeleteHandler); + + $('char-main-add').onclick = () => { + const div = document.createElement('div'); + div.className = 'struct-row char-main-item'; + setHtml(div, ''); + addDeleteHandler(div); + $('char-main-list').appendChild(div); + }; + + $('char-rel-add').onclick = () => { + const div = document.createElement('div'); + div.className = 'struct-row char-rel-item'; + setHtml(div, ` + + + + + `); + addDeleteHandler(div); + $('char-rel-list').appendChild(div); + }; + } + + function openEditor(section) { + currentEditSection = section; + const meta = SECTION_META[section]; + const es = $('editor-struct'); + const ta = $('editor-ta'); + + $('editor-title').textContent = meta.title; + $('editor-hint').textContent = meta.hint; + $('editor-err').classList.remove('visible'); + $('editor-err').textContent = ''; + es.classList.add('hidden'); + ta.classList.remove('hidden'); + + if (section === 'keywords') { + ta.value = summaryData.keywords.map(k => `${k.text}|${k.weight || '一般'}`).join('\n'); + } else if (section === 'facts') { + ta.value = (summaryData.facts || []) + .filter(f => !f.retracted) + .map(f => { + const parts = [f.s, f.p, f.o]; + if (f.trend) parts.push(f.trend); + return parts.join('|'); + }) + .join('\n'); + } else { + ta.classList.add('hidden'); + es.classList.remove('hidden'); + if (section === 'events') renderEventsEditor(summaryData.events || []); + else if (section === 'characters') renderCharactersEditor(summaryData.characters || { main: [], relationships: [] }); + else if (section === 'arcs') renderArcsEditor(summaryData.arcs || []); + } + + $('editor-modal').classList.add('active'); + postMsg('EDITOR_OPENED'); + } + + function closeEditor() { + $('editor-modal').classList.remove('active'); + currentEditSection = null; + postMsg('EDITOR_CLOSED'); + } + + function saveEditor() { + const section = currentEditSection; + const es = $('editor-struct'); + const ta = $('editor-ta'); + let parsed; + + try { + if (section === 'keywords') { + const oldMap = new Map((summaryData.keywords || []).map(k => [k.text, k])); + parsed = ta.value.trim().split('\n').filter(l => l.trim()).map(line => { + const [text, weight] = line.split('|').map(s => s.trim()); + return preserveAddedAt({ text: text || '', weight: weight || '一般' }, oldMap.get(text)); + }); + } else if (section === 'events') { + const oldMap = new Map((summaryData.events || []).map(e => [e.id, e])); + parsed = Array.from(es.querySelectorAll('.event-item')).map(it => { + const id = it.dataset.id; + return preserveAddedAt({ + id, + title: it.querySelector('.event-title').value.trim(), + timeLabel: it.querySelector('.event-time').value.trim(), + summary: it.querySelector('.event-summary').value.trim(), + participants: it.querySelector('.event-participants').value.trim().split(/[,、,]/).map(s => s.trim()).filter(Boolean), + type: it.querySelector('.event-type').value, + weight: it.querySelector('.event-weight').value + }, oldMap.get(id)); + }).filter(e => e.title || e.summary); + } else if (section === 'characters') { + const oldMainMap = new Map((summaryData.characters?.main || []).map(m => [getCharName(m), m])); + const mainNames = Array.from(es.querySelectorAll('.char-main-name')).map(i => i.value.trim()).filter(Boolean); + const main = mainNames.map(n => preserveAddedAt({ name: n }, oldMainMap.get(n))); + + const oldRelMap = new Map((summaryData.characters?.relationships || []).map(r => [`${r.from}->${r.to}`, r])); + const rels = Array.from(es.querySelectorAll('.char-rel-item')).map(it => { + const from = it.querySelector('.char-rel-from').value.trim(); + const to = it.querySelector('.char-rel-to').value.trim(); + return preserveAddedAt({ + from, to, + label: it.querySelector('.char-rel-label').value.trim(), + trend: it.querySelector('.char-rel-trend').value + }, oldRelMap.get(`${from}->${to}`)); + }).filter(r => r.from && r.to); + + parsed = { main, relationships: rels }; + } else if (section === 'arcs') { + const oldArcMap = new Map((summaryData.arcs || []).map(a => [a.name, a])); + parsed = Array.from(es.querySelectorAll('.arc-item')).map(it => { + const name = it.querySelector('.arc-name').value.trim(); + const oldArc = oldArcMap.get(name); + const oldMomentMap = new Map((oldArc?.moments || []).map(m => [typeof m === 'string' ? m : m.text, m])); + const momentsRaw = it.querySelector('.arc-moments').value.trim(); + const moments = momentsRaw ? momentsRaw.split('\n').map(s => s.trim()).filter(Boolean).map(t => preserveAddedAt({ text: t }, oldMomentMap.get(t))) : []; + return preserveAddedAt({ + name, + trajectory: it.querySelector('.arc-trajectory').value.trim(), + progress: Math.max(0, Math.min(1, (parseFloat(it.querySelector('.arc-progress').value) || 0) / 100)), + moments + }, oldArc); + }).filter(a => a.name || a.trajectory || a.moments?.length); + } else if (section === 'facts') { + const oldMap = new Map((summaryData.facts || []).map(f => [`${f.s}::${f.p}`, f])); + parsed = ta.value + .split('\n') + .map(l => l.trim()) + .filter(Boolean) + .map(line => { + const parts = line.split('|').map(s => s.trim()); + const s = parts[0]; + const p = parts[1]; + const o = parts[2]; + const trend = parts[3]; + if (!s || !p) return null; + if (!o) return null; + const key = `${s}::${p}`; + const old = oldMap.get(key); + const fact = { + id: old?.id || `f-${Date.now()}`, + s, p, o, + since: old?.since ?? 0, + _addedAt: old?._addedAt ?? 0, + }; + if (/^对.+的/.test(p) && trend) { + fact.trend = trend; + } + return fact; + }) + .filter(Boolean); + } + } catch (e) { + $('editor-err').textContent = `格式错误: ${e.message}`; + $('editor-err').classList.add('visible'); + return; + } + + postMsg('UPDATE_SECTION', { section, data: parsed }); + + if (section === 'keywords') renderKeywords(parsed); + else if (section === 'events') { renderTimeline(parsed); $('stat-events').textContent = parsed.length; } + else if (section === 'characters') renderRelations(parsed); + else if (section === 'arcs') renderArcs(parsed); + else if (section === 'facts') renderFacts(parsed); + + closeEditor(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Message Handler + // ═══════════════════════════════════════════════════════════════════════════ + + function handleParentMessage(e) { + if (e.origin !== PARENT_ORIGIN || e.source !== window.parent) return; + + const d = e.data; + if (!d || d.source !== 'LittleWhiteBox') return; + + const btn = $('btn-generate'); + + switch (d.type) { + case 'GENERATION_STATE': + localGenerating = !!d.isGenerating; + btn.textContent = localGenerating ? '停止' : '总结'; + break; + + case 'SUMMARY_BASE_DATA': + if (d.stats) { + updateStats(d.stats); + $('summarized-count').textContent = d.stats.hiddenCount ?? 0; + } + if (d.hideSummarized !== undefined) $('hide-summarized').checked = d.hideSummarized; + if (d.keepVisibleCount !== undefined) $('keep-visible-count').value = d.keepVisibleCount; + break; + + case 'SUMMARY_FULL_DATA': + if (d.payload) { + const p = d.payload; + if (p.keywords) renderKeywords(p.keywords); + if (p.events) renderTimeline(p.events); + if (p.characters) renderRelations(p.characters); + if (p.arcs) renderArcs(p.arcs); + if (p.facts) renderFacts(p.facts); + $('stat-events').textContent = p.events?.length || 0; + if (p.lastSummarizedMesId != null) $('stat-summarized').textContent = p.lastSummarizedMesId + 1; + if (p.stats) updateStats(p.stats); + } + break; + + case 'SUMMARY_ERROR': + console.error('Summary error:', d.message); + break; + + case 'SUMMARY_CLEARED': { + const t = d.payload?.totalFloors || 0; + $('stat-events').textContent = 0; + $('stat-summarized').textContent = 0; + $('stat-pending').textContent = t; + $('summarized-count').textContent = 0; + summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] }; + renderKeywords([]); + renderTimeline([]); + renderRelations(null); + renderArcs([]); + renderFacts([]); + break; + } + + case 'LOAD_PANEL_CONFIG': + if (d.config) applyConfig(d.config); + break; + + case 'VECTOR_CONFIG': + if (d.config) loadVectorConfig(d.config); + break; + + case 'VECTOR_ONLINE_STATUS': + updateOnlineStatus(d.status, d.message); + break; + + case 'VECTOR_STATS': + updateVectorStats(d.stats); + if (d.mismatch !== undefined) showVectorMismatchWarning(d.mismatch); + break; + + case 'ANCHOR_STATS': + updateAnchorStats(d.stats || {}); + break; + + case 'ANCHOR_GEN_PROGRESS': + updateAnchorProgress(d.current, d.total, d.message); + break; + + case 'VECTOR_GEN_PROGRESS': { + const progress = $('vector-gen-progress'); + const btnGen = $('btn-gen-vectors'); + const btnCancel = $('btn-cancel-vectors'); + const btnClear = $('btn-clear-vectors'); + + if (d.current < 0) { + progress.classList.add('hidden'); + btnGen.classList.remove('hidden'); + btnCancel.classList.add('hidden'); + btnClear.classList.remove('hidden'); + vectorGenerating = false; + } else { + vectorGenerating = true; + progress.classList.remove('hidden'); + btnGen.classList.add('hidden'); + btnCancel.classList.remove('hidden'); + btnClear.classList.add('hidden'); + + const percent = d.total > 0 ? Math.round(d.current / d.total * 100) : 0; + progress.querySelector('.progress-inner').style.width = percent + '%'; + const displayText = d.message || `${d.phase || ''}: ${d.current}/${d.total}`; + progress.querySelector('.progress-text').textContent = displayText; + } + break; + } + + case 'VECTOR_EXPORT_RESULT': + $('btn-export-vectors').disabled = false; + if (d.success) { + $('vector-io-status').textContent = `导出成功: ${d.filename} (${(d.size / 1024 / 1024).toFixed(2)}MB)`; + } else { + $('vector-io-status').textContent = '导出失败: ' + (d.error || '未知错误'); + } + break; + + case 'VECTOR_IMPORT_RESULT': + $('btn-import-vectors').disabled = false; + if (d.success) { + let msg = `导入成功: ${d.chunkCount} 片段, ${d.eventCount} 事件`; + if (d.warnings?.length) { + msg += '\n⚠️ ' + d.warnings.join('\n⚠️ '); + } + $('vector-io-status').textContent = msg; + // 刷新统计 + postMsg('REQUEST_VECTOR_STATS'); + } else { + $('vector-io-status').textContent = '导入失败: ' + (d.error || '未知错误'); + } + break; + + case 'RECALL_LOG': + setRecallLog(d.text || ''); + break; + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Event Bindings + // ═══════════════════════════════════════════════════════════════════════════ + + function bindEvents() { + // Section edit buttons + $$('.sec-btn[data-section]').forEach(b => b.onclick = () => openEditor(b.dataset.section)); + + // Editor modal + $('editor-backdrop').onclick = closeEditor; + $('editor-close').onclick = closeEditor; + $('editor-cancel').onclick = closeEditor; + $('editor-save').onclick = saveEditor; + + // Settings modal + $('btn-settings').onclick = openSettings; + $('settings-backdrop').onclick = () => closeSettings(false); + $('settings-close').onclick = () => closeSettings(false); + $('settings-cancel').onclick = () => closeSettings(false); + $('settings-save').onclick = () => closeSettings(true); + + // Settings tabs + $$('.settings-tab').forEach(tab => { + tab.onclick = () => { + const targetId = tab.dataset.tab; + if (!targetId) return; + + // Update tab active state + $$('.settings-tab').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + // Update pane active state + $$('.tab-pane').forEach(p => p.classList.remove('active')); + $(targetId).classList.add('active'); + + // If switching to debug tab, refresh log + if (targetId === 'tab-debug') { + postMsg('REQUEST_RECALL_LOG'); + } + }; + }); + + // API provider change + $('api-provider').onchange = e => { + const pv = PROVIDER_DEFAULTS[e.target.value]; + $('api-url').value = ''; + if (!pv.canFetch) config.api.modelCache = []; + updateProviderUI(e.target.value); + }; + + $('btn-connect').onclick = fetchModels; + $('api-model-select').onchange = e => { config.api.model = e.target.value; }; + + // Trigger timing + $('trigger-timing').onchange = e => { + const en = $('trigger-enabled'); + if (e.target.value === 'manual') { + en.checked = false; + en.disabled = true; + en.parentElement.style.opacity = '.5'; + } else { + en.disabled = false; + en.parentElement.style.opacity = '1'; + } + }; + + // 总结间隔范围校验 + $('trigger-interval').onchange = e => { + let val = parseInt(e.target.value) || 20; + val = Math.max(1, Math.min(30, val)); + e.target.value = val; + }; + + // Main actions + $('btn-clear').onclick = () => postMsg('REQUEST_CLEAR'); + $('btn-generate').onclick = () => { + const btn = $('btn-generate'); + if (!localGenerating) { + localGenerating = true; + btn.textContent = '停止'; + postMsg('REQUEST_GENERATE', { config: { api: config.api, gen: config.gen, trigger: config.trigger } }); + } else { + localGenerating = false; + btn.textContent = '总结'; + postMsg('REQUEST_CANCEL'); + } + }; + + // Hide summarized + $('hide-summarized').onchange = e => postMsg('TOGGLE_HIDE_SUMMARIZED', { enabled: e.target.checked }); + $('keep-visible-count').onchange = e => { + const c = Math.max(0, Math.min(50, parseInt(e.target.value) || 3)); + e.target.value = c; + postMsg('UPDATE_KEEP_VISIBLE', { count: c }); + }; + + // Fullscreen relations + $('btn-fullscreen-relations').onclick = openRelationsFullscreen; + $('rel-fs-backdrop').onclick = closeRelationsFullscreen; + $('rel-fs-close').onclick = closeRelationsFullscreen; + + // HF guide + + // Character selector + $('char-sel-trigger').onclick = e => { + e.stopPropagation(); + $('char-sel').classList.toggle('open'); + }; + + document.onclick = e => { + const cs = $('char-sel'); + if (cs && !cs.contains(e.target)) cs.classList.remove('open'); + }; + + // Vector UI + initVectorUI(); + + // Gen params collapsible + const genParamsToggle = $('gen-params-toggle'); + const genParamsContent = $('gen-params-content'); + if (genParamsToggle && genParamsContent) { + genParamsToggle.onclick = () => { + const collapse = genParamsToggle.closest('.settings-collapse'); + collapse.classList.toggle('open'); + genParamsContent.classList.toggle('hidden'); + }; + } + + // Filter rules collapsible + const filterRulesToggle = $('filter-rules-toggle'); + const filterRulesContent = $('filter-rules-content'); + if (filterRulesToggle && filterRulesContent) { + filterRulesToggle.onclick = () => { + const collapse = filterRulesToggle.closest('.settings-collapse'); + collapse.classList.toggle('open'); + filterRulesContent.classList.toggle('hidden'); + }; + } + + // Auto summary sub-options toggle + const triggerEnabled = $('trigger-enabled'); + const autoSummaryOptions = $('auto-summary-options'); + if (triggerEnabled && autoSummaryOptions) { + triggerEnabled.onchange = () => { + autoSummaryOptions.classList.toggle('hidden', !triggerEnabled.checked); + }; + } + + // Force insert sub-options toggle + const triggerInsertAtEnd = $('trigger-insert-at-end'); + const insertWrapperOptions = $('insert-wrapper-options'); + if (triggerInsertAtEnd && insertWrapperOptions) { + triggerInsertAtEnd.onchange = () => { + insertWrapperOptions.classList.toggle('hidden', !triggerInsertAtEnd.checked); + }; + } + + + // Resize + window.onresize = () => { + relationChart?.resize(); + relationChartFullscreen?.resize(); + }; + + // Parent messages + window.onmessage = handleParentMessage; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Init + // ═══════════════════════════════════════════════════════════════════════════ + + function init() { + loadConfig(); + + // Initial state + $('stat-events').textContent = '—'; + $('stat-summarized').textContent = '—'; + $('stat-pending').textContent = '—'; + $('summarized-count').textContent = '0'; + + renderKeywords([]); + renderTimeline([]); + renderArcs([]); + renderFacts([]); + + bindEvents(); + + // === THEME SWITCHER === + (function () { + const STORAGE_KEY = 'xb-theme-alt'; + const CSS_MAP = { default: 'story-summary.css', dark: 'story-summary.css', neo: 'story-summary-a.css', 'neo-dark': 'story-summary-a.css' }; + const link = document.querySelector('link[rel="stylesheet"]'); + const sel = document.getElementById('theme-select'); + if (!link || !sel) return; + + function applyTheme(theme) { + if (!CSS_MAP[theme]) return; + link.setAttribute('href', CSS_MAP[theme]); + document.documentElement.setAttribute('data-theme', (theme === 'dark' || theme === 'neo-dark') ? 'dark' : ''); + } + + // 启动时恢复主题 + const saved = localStorage.getItem(STORAGE_KEY) || 'default'; + applyTheme(saved); + sel.value = saved; + + // 下拉框切换 + sel.addEventListener('change', function () { + const theme = sel.value; + applyTheme(theme); + localStorage.setItem(STORAGE_KEY, theme); + console.log(`[Theme] Switched → ${theme} (${CSS_MAP[theme]})`); + }); + })(); + // === END THEME SWITCHER === + + // Notify parent + postMsg('FRAME_READY'); + } + + // Start + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + + function renderFacts(facts) { + summaryData.facts = facts || []; + + const container = $('facts-list'); + if (!container) return; + + const isRelation = f => /^对.+的/.test(f.p); + const stateFacts = (facts || []).filter(f => !f.retracted && !isRelation(f)); + + if (!stateFacts.length) { + setHtml(container, '
暂无状态记录
'); + return; + } + + const grouped = new Map(); + for (const f of stateFacts) { + if (!grouped.has(f.s)) grouped.set(f.s, []); + grouped.get(f.s).push(f); + } + + let html = ''; + for (const [subject, items] of grouped) { + html += `
+
${h(subject)}
+ ${items.map(f => ` +
+ ${h(f.p)} + ${h(f.o)} + #${(f.since || 0) + 1} +
+ `).join('')} +
`; + } + + setHtml(container, html); + } +})(); diff --git a/modules/story-summary/story-summary.css b/modules/story-summary/story-summary.css new file mode 100644 index 0000000..3e26a78 --- /dev/null +++ b/modules/story-summary/story-summary.css @@ -0,0 +1,3459 @@ +/* story-summary.css */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Facts (替换 World State) + ═══════════════════════════════════════════════════════════════════════════ */ + +.facts { + flex: 0 0 auto; +} + +.facts-list { + max-height: 200px; + overflow-y: auto; + padding-right: 4px; +} + +.fact-group { + margin-bottom: 12px; +} + +.fact-group:last-child { + margin-bottom: 0; +} + +.fact-group-title { + font-size: 0.75rem; + font-weight: 600; + color: var(--hl); + margin-bottom: 6px; + padding-bottom: 4px; + border-bottom: 1px dashed var(--bdr2); +} + +.fact-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + margin-bottom: 4px; + background: var(--bg3); + border: 1px solid var(--bdr2); + border-radius: 4px; + font-size: 0.8125rem; +} + +.fact-predicate { + color: var(--txt2); + min-width: 60px; +} + +.fact-predicate::after { + content: ':'; +} + +.fact-object { + color: var(--txt); + flex: 1; +} + +.fact-since { + font-size: 0.625rem; + color: var(--txt3); +} + +@media (max-width: 768px) { + .facts-list { + max-height: 180px; + } + + .fact-item { + padding: 6px 8px; + font-size: 0.75rem; + } +} + +:root { + /* ── Base ── */ + --bg: #fafafa; + --bg2: #fff; + --bg3: #f5f5f5; + --txt: #1a1a1a; + --txt2: #444; + --txt3: #666; + --bdr: #dcdcdc; + --bdr2: #e8e8e8; + --acc: #1a1a1a; + --hl: #d87a7a; + --hl2: #d85858; + --hl-soft: rgba(184, 90, 90, .1); + --inv: #fff; + /* text on accent/primary bg */ + + /* ── Buttons ── */ + --btn-p-hover: #555; + --btn-p-disabled: #999; + + /* ── Status ── */ + --warn: #ff9800; + --success: #22c55e; + --info: #3b82f6; + --downloading: #f59e0b; + --error: #ef4444; + + /* ── Code blocks ── */ + --code-bg: #1e1e1e; + --code-txt: #d4d4d4; + --muted: #999; + + /* ── Overlay ── */ + --overlay: rgba(0, 0, 0, .5); + + /* ── Tag highlight border ── */ + --tag-s-bdr: rgba(255, 68, 68, .2); + --tag-shadow: rgba(0, 0, 0, .08); + + /* ── Category colors ── */ + --cat-status: #e57373; + --cat-inventory: #64b5f6; + --cat-relation: #ba68c8; + --cat-knowledge: #4db6ac; + --cat-rule: #ffd54f; + + /* ── Trend colors ── */ + --trend-broken: #444; + --trend-broken-bg: rgba(68, 68, 68, .15); + --trend-hate: #8b0000; + --trend-hate-bg: rgba(139, 0, 0, .15); + --trend-dislike: #cd5c5c; + --trend-dislike-bg: rgba(205, 92, 92, .15); + --trend-stranger: #888; + --trend-stranger-bg: rgba(136, 136, 136, .15); + --trend-click: #4a9a7e; + --trend-click-bg: rgba(102, 205, 170, .15); + --trend-close-bg: rgba(235, 106, 106, .15); + --trend-merge: #c71585; + --trend-merge-bg: rgba(199, 21, 133, .2); +} + +:root[data-theme="dark"] { + /* ── Base ── */ + --bg: #121212; + --bg2: #1e1e1e; + --bg3: #2a2a2a; + --txt: #e0e0e0; + --txt2: #b0b0b0; + --txt3: #808080; + --bdr: #3a3a3a; + --bdr2: #333; + --acc: #e0e0e0; + --hl: #e8928a; + --hl2: #e07070; + --hl-soft: rgba(232, 146, 138, .12); + --inv: #1e1e1e; + + /* ── Buttons ── */ + --btn-p-hover: #ccc; + --btn-p-disabled: #666; + + /* ── Status ── */ + --warn: #ffb74d; + --success: #4caf50; + --info: #64b5f6; + --downloading: #ffa726; + --error: #ef5350; + + /* ── Code blocks ── */ + --code-bg: #0d0d0d; + --code-txt: #d4d4d4; + --muted: #777; + + /* ── Overlay ── */ + --overlay: rgba(0, 0, 0, .7); + + /* ── Tag ── */ + --tag-s-bdr: rgba(232, 146, 138, .3); + --tag-shadow: rgba(0, 0, 0, .3); + + /* ── Category colors (softer for dark) ── */ + --cat-status: #ef9a9a; + --cat-inventory: #90caf9; + --cat-relation: #ce93d8; + --cat-knowledge: #80cbc4; + --cat-rule: #ffe082; + + /* ── Trend colors ── */ + --trend-broken: #999; + --trend-broken-bg: rgba(153, 153, 153, .15); + --trend-hate: #ef5350; + --trend-hate-bg: rgba(239, 83, 80, .15); + --trend-dislike: #e57373; + --trend-dislike-bg: rgba(229, 115, 115, .15); + --trend-stranger: #aaa; + --trend-stranger-bg: rgba(170, 170, 170, .12); + --trend-click: #66bb6a; + --trend-click-bg: rgba(102, 187, 106, .15); + --trend-close-bg: rgba(232, 146, 138, .15); + --trend-merge: #f06292; + --trend-merge-bg: rgba(240, 98, 146, .15); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif; + background: var(--bg); + color: var(--txt); + line-height: 1.6; + min-height: 100vh; + -webkit-overflow-scrolling: touch; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Layout + ═══════════════════════════════════════════════════════════════════════════ */ + +.container { + display: flex; + flex-direction: column; + min-height: 100vh; + padding: 24px 40px; + max-width: 1800px; + margin: 0 auto; +} + +header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding-bottom: 24px; + border-bottom: 1px solid var(--bdr); + margin-bottom: 24px; +} + +main { + display: grid; + grid-template-columns: 1fr 480px; + gap: 24px; + flex: 1; + min-height: 0; +} + +.left, +.right { + display: flex; + flex-direction: column; + gap: 24px; + min-height: 0; +} + +/* 关键词卡片:固定高度 */ +.left>.card:first-child { + flex: 0 0 auto; + /* 关键词:不伸缩 */ +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Typography + ═══════════════════════════════════════════════════════════════════════════ */ + +h1 { + font-size: 2rem; + font-weight: 300; + letter-spacing: -.02em; + margin-bottom: 4px; +} + +h1 span { + font-weight: 600; +} + +.subtitle { + font-size: .875rem; + color: var(--txt3); + letter-spacing: .05em; + text-transform: uppercase; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Stats + ═══════════════════════════════════════════════════════════════════════════ */ + +.stats { + display: flex; + gap: 48px; + text-align: right; +} + +.stat { + display: flex; + flex-direction: column; +} + +.stat-val { + font-size: 2.5rem; + font-weight: 200; + line-height: 1; + letter-spacing: -.03em; +} + +.stat-val .hl { + color: var(--hl); +} + +.stat-lbl { + font-size: .75rem; + color: var(--txt3); + text-transform: uppercase; + letter-spacing: .1em; + margin-top: 4px; +} + +.stat-warning { + font-size: .625rem; + color: var(--warn); + margin-top: 4px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Controls + ═══════════════════════════════════════════════════════════════════════════ */ + +.controls { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 0; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.spacer { + flex: 1; +} + +.chk-label { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + background: transparent; + border: none; + font-size: .8125rem; + color: var(--txt2); + cursor: pointer; + transition: all .2s; +} + +.chk-label:hover { + color: var(--txt); +} + +.chk-label input { + width: 16px; + height: 16px; + accent-color: var(--hl); + cursor: pointer; +} + +.chk-label strong { + color: var(--hl); +} + +#keep-visible-count { + width: 3.5em; + min-width: 3em; + max-width: 4em; + padding: 4px 6px; + margin: 0 4px; + background: var(--bg2); + border: 1px solid var(--bdr); + font-size: inherit; + font-weight: bold; + color: var(--hl); + text-align: center; + border-radius: 3px; + font-variant-numeric: tabular-nums; + + /* 禁用 number input 的 spinner(PC 上会挤掉数字) */ + -moz-appearance: textfield; + appearance: textfield; +} + +#keep-visible-count::-webkit-outer-spin-button, +#keep-visible-count::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +#keep-visible-count:focus { + border-color: var(--acc); + outline: none; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Buttons + ═══════════════════════════════════════════════════════════════════════════ */ + +.btn { + padding: 12px 28px; + background: var(--bg2); + color: var(--txt); + border: 1px solid var(--bdr); + font-size: .875rem; + font-weight: 500; + cursor: pointer; + transition: all .2s; +} + +.btn:hover { + border-color: var(--acc); + background: var(--bg3); +} + +.btn-p { + background: var(--acc); + color: var(--inv); + border-color: var(--acc); +} + +.btn-p:hover { + background: var(--btn-p-hover); +} + +.btn-p:disabled { + background: var(--btn-p-disabled); + border-color: var(--btn-p-disabled); + cursor: not-allowed; + opacity: .7; +} + +.btn-icon { + padding: 10px 16px; + display: flex; + align-items: center; + gap: 6px; +} + +.btn-icon svg { + width: 16px; + height: 16px; +} + +.btn-sm { + padding: 8px 16px; + font-size: .8125rem; +} + +.btn-del { + background: transparent; + color: var(--hl); + border-color: var(--hl); +} + +.btn-del:hover { + background: var(--hl-soft); +} + +.btn-group { + display: flex; + gap: 8px; + flex-wrap: nowrap; +} + +.btn-group .btn { + flex: 1; + min-width: 0; + padding: 10px 14px; + text-align: center; + white-space: nowrap; +} + +.btn-group .btn-icon { + padding: 10px 14px; +} + +.btn-debug { + background: var(--bg2); + color: var(--txt2); + border: 1px solid var(--bdr); + display: flex; + align-items: center; + gap: 6px; + justify-content: center; +} + +.btn-debug svg { + width: 14px; + height: 14px; +} + +.btn-debug:hover { + background: var(--bg3); + border-color: var(--acc); + color: var(--txt); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Cards & Sections + ═══════════════════════════════════════════════════════════════════════════ */ + +.card { + background: var(--bg2); + border: 1px solid var(--bdr); + padding: 24px; +} + +.sec-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.sec-title { + font-size: .75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .15em; + color: var(--txt2); +} + +.sec-btn { + padding: 4px 12px; + background: transparent; + border: 1px solid var(--bdr); + font-size: .6875rem; + color: var(--txt3); + cursor: pointer; + transition: all .2s; + text-transform: uppercase; + letter-spacing: .05em; +} + +.sec-btn:hover { + border-color: var(--acc); + color: var(--txt); + background: var(--bg3); +} + +.sec-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.sec-icon { + padding: 4px 8px; + display: flex; + align-items: center; + justify-content: center; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Keywords + ═══════════════════════════════════════════════════════════════════════════ */ + +.keywords { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 16px; +} + +.tag { + padding: 8px 20px; + background: var(--bg3); + border: 1px solid var(--bdr2); + font-size: .875rem; + color: var(--txt2); + transition: all .2s; + cursor: default; +} + +.tag.p { + background: var(--acc); + color: var(--inv); + border-color: var(--acc); + font-weight: 500; +} + +.tag.s { + background: var(--hl-soft); + border-color: var(--tag-s-bdr); + color: var(--hl); +} + +.tag:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--tag-shadow); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Timeline + ═══════════════════════════════════════════════════════════════════════════ */ + +.timeline { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + max-height: 1140px; +} + +.tl-list { + flex: 1; + overflow-y: auto; + padding-right: 8px; + min-height: 0; +} + +.tl-list::-webkit-scrollbar, +.scroll::-webkit-scrollbar { + width: 4px; +} + +.tl-list::-webkit-scrollbar-thumb, +.scroll::-webkit-scrollbar-thumb { + background: var(--bdr); +} + +.tl-item { + position: relative; + padding-left: 32px; + padding-bottom: 32px; + border-left: 1px solid var(--bdr); + margin-left: 8px; +} + +.tl-item:last-child { + border-left-color: transparent; + padding-bottom: 0; +} + +.tl-dot { + position: absolute; + left: -5px; + top: 0; + width: 9px; + height: 9px; + background: var(--bg2); + border: 2px solid var(--txt3); + border-radius: 50%; + transition: all .2s; +} + +.tl-item:hover .tl-dot { + border-color: var(--hl); + background: var(--hl); + transform: scale(1.3); +} + +.tl-item.crit .tl-dot { + border-color: var(--hl); + background: var(--hl); +} + +.tl-head { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 8px; +} + +.tl-title { + font-size: 1rem; + font-weight: 500; +} + +.tl-time { + font-size: .75rem; + color: var(--txt3); + font-variant-numeric: tabular-nums; +} + +.tl-brief { + font-size: .875rem; + color: var(--txt2); + line-height: 1.7; + margin-bottom: 12px; +} + +.tl-meta { + display: flex; + gap: 16px; + font-size: .75rem; + color: var(--txt3); +} + +.tl-meta .imp { + color: var(--hl); + font-weight: 500; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Relations Chart + ═══════════════════════════════════════════════════════════════════════════ */ + +.relations { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + max-height: 480px; +} + +#relation-chart, +#relation-chart-fullscreen { + width: 100%; + flex: 1; + min-height: 0; + touch-action: none; +} + + +/* ═══════════════════════════════════════════════════════════════════════════ + Profile + ═══════════════════════════════════════════════════════════════════════════ */ + +.profile { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + max-height: 480px; +} + +.profile-content { + flex: 1; + overflow-y: auto; + padding-right: 8px; + min-height: 0; +} + +.prof-arc { + padding: 16px; + margin-bottom: 24px; +} + +.prof-name { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 4px; +} + +.prof-traj { + font-size: .8125rem; + color: var(--txt3); + line-height: 1.5; +} + +.prof-prog-wrap { + margin-bottom: 16px; +} + +.prof-prog-lbl { + display: flex; + justify-content: space-between; + font-size: .75rem; + color: var(--txt3); + margin-bottom: 6px; +} + +.prof-prog { + height: 4px; + background: var(--bdr); + border-radius: 2px; + overflow: hidden; +} + +.prof-prog-inner { + height: 100%; + background: linear-gradient(90deg, var(--hl), var(--hl2)); + border-radius: 2px; + transition: width .6s; +} + +.prof-moments { + background: var(--bg2); + border-left: 3px solid var(--hl); + padding: 12px 16px; +} + +.prof-moments-title { + font-size: .6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .1em; + color: var(--txt3); + margin-bottom: 8px; +} + +.prof-moment { + position: relative; + padding-left: 16px; + margin-bottom: 6px; + font-size: .8125rem; + color: var(--txt2); + line-height: 1.5; +} + +.prof-moment::before { + content: ''; + position: absolute; + left: 0; + top: 7px; + width: 6px; + height: 6px; + background: var(--hl); + border-radius: 50%; +} + +.prof-moment:last-child { + margin-bottom: 0; +} + +.prof-rels { + display: flex; + flex-direction: column; +} + +.rels-group { + border-bottom: 1px solid var(--bdr2); + padding: 16px 0; +} + +.rels-group:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.rels-group:first-child { + padding-top: 0; +} + +.rels-group-title { + font-size: .75rem; + font-weight: 600; + color: var(--txt3); + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.rel-item { + display: flex; + align-items: baseline; + gap: 8px; + padding: 4px 8px; + border-radius: 4px; + margin-bottom: 2px; +} + +.rel-item:hover { + background: var(--bg3); +} + +.rel-target { + font-size: .9rem; + color: var(--txt2); + white-space: nowrap; + min-width: 60px; +} + +.rel-label { + font-size: .7rem; + line-height: 1.5; + flex: 1; +} + +.rel-trend { + font-size: .6875rem; + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; +} + +.trend-broken { + background: var(--trend-broken-bg); + color: var(--trend-broken); +} + +.trend-hate { + background: var(--trend-hate-bg); + color: var(--trend-hate); +} + +.trend-dislike { + background: var(--trend-dislike-bg); + color: var(--trend-dislike); +} + +.trend-stranger { + background: var(--trend-stranger-bg); + color: var(--trend-stranger); +} + +.trend-click { + background: var(--trend-click-bg); + color: var(--trend-click); +} + +.trend-close { + background: var(--trend-close-bg); + color: var(--hl); +} + +.trend-merge { + background: var(--trend-merge-bg); + color: var(--trend-merge); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Custom Select + ═══════════════════════════════════════════════════════════════════════════ */ + +.custom-select { + position: relative; + min-width: 140px; + font-size: .8125rem; +} + +.sel-trigger { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + background: var(--bg3); + border: 1px solid var(--bdr); + border-radius: 6px; + cursor: pointer; + transition: all .2s; + user-select: none; +} + +.sel-trigger:hover { + border-color: var(--acc); + background: var(--bg2); +} + +.sel-trigger::after { + content: ''; + width: 16px; + height: 16px; + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238d8d8d' stroke-width='2'%3e%3cpath d='M6 9l6 6 6-6'/%3e%3c/svg%3e") center/16px no-repeat; + transition: transform .2s; +} + +.custom-select.open .sel-trigger::after { + transform: rotate(180deg); +} + +.sel-opts { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background: var(--bg2); + border: 1px solid var(--bdr); + border-radius: 6px; + box-shadow: 0 4px 20px rgba(0, 0, 0, .15); + z-index: 100; + display: none; + max-height: 240px; + overflow-y: auto; + padding: 4px; +} + +.custom-select.open .sel-opts { + display: block; + animation: fadeIn .2s; +} + +.sel-opt { + padding: 8px 12px; + cursor: pointer; + border-radius: 4px; + transition: background .1s; +} + +.sel-opt:hover { + background: var(--bg3); +} + +.sel-opt.sel { + background: var(--hl-soft); + color: var(--hl); + font-weight: 600; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Modal + ═══════════════════════════════════════════════════════════════════════════ */ + +.modal { + position: fixed; + inset: 0; + z-index: 10000; + display: none; + align-items: center; + justify-content: center; +} + +.modal.active { + display: flex; +} + +.modal-bg { + position: absolute; + inset: 0; + background: var(--overlay); + backdrop-filter: blur(4px); +} + +.modal-box { + position: relative; + width: 100%; + max-width: 720px; + max-height: 90vh; + background: var(--bg2); + border: 1px solid var(--bdr); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.modal-head { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid var(--bdr); +} + +.modal-head h2 { + font-size: 1rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .1em; +} + +.modal-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid var(--bdr); + cursor: pointer; + transition: all .2s; +} + +.modal-close:hover { + background: var(--bg3); + border-color: var(--acc); +} + +.modal-close svg { + width: 14px; + height: 14px; + color: var(--txt); +} + +.modal-body { + flex: 1; + overflow-y: auto; + padding: 24px; +} + +.modal-foot { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 24px; + border-top: 1px solid var(--bdr); +} + +.fullscreen .modal-box { + width: 95vw; + height: 90vh; + max-width: none; + max-height: none; +} + +.fullscreen .modal-body { + flex: 1; + padding: 0; + overflow: hidden; +} + +#relation-chart-fullscreen { + width: 100%; + height: 100%; + min-height: 500px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Editor + ═══════════════════════════════════════════════════════════════════════════ */ + +.editor-ta { + width: 100%; + min-height: 300px; + padding: 16px; + background: var(--bg3); + border: 1px solid var(--bdr); + font-family: 'SF Mono', Monaco, Consolas, monospace; + font-size: .8125rem; + line-height: 1.6; + color: var(--txt); + resize: vertical; + outline: none; +} + +.editor-ta:focus { + border-color: var(--acc); +} + +.editor-hint { + font-size: .75rem; + color: var(--txt3); + margin-bottom: 12px; + line-height: 1.5; +} + +.editor-err { + padding: 12px; + background: var(--hl-soft); + border: 1px solid var(--tag-s-bdr); + color: var(--hl); + font-size: .8125rem; + margin-top: 12px; + display: none; +} + +.editor-err.visible { + display: block; +} + +.struct-item { + border: 1px solid var(--bdr); + background: var(--bg3); + padding: 12px; + margin-bottom: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.struct-row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.struct-row input, +.struct-row select, +.struct-row textarea { + flex: 1; + min-width: 0; + padding: 8px 10px; + background: var(--bg2); + border: 1px solid var(--bdr); + font-size: .8125rem; + color: var(--txt); + outline: none; + transition: border-color .2s; +} + +.struct-row input:focus, +.struct-row select:focus, +.struct-row textarea:focus { + border-color: var(--acc); +} + +.struct-row textarea { + resize: vertical; + font-family: inherit; + min-height: 60px; +} + +.struct-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 4px; +} + +.struct-actions span { + font-size: .75rem; + color: var(--txt3); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Settings + ═══════════════════════════════════════════════════════════════════════════ */ + +.settings-section { + margin-bottom: 32px; +} + +.settings-section:last-child { + margin-bottom: 0; +} + +.settings-section-title { + font-size: .6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .15em; + color: var(--txt3); + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--bdr2); +} + +.settings-row { + display: flex; + gap: 16px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.settings-row:last-child { + margin-bottom: 0; +} + +.settings-field { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 200px; +} + +.settings-field.full { + flex: 100%; +} + +.settings-field label { + font-size: .75rem; + color: var(--txt3); + text-transform: uppercase; + letter-spacing: .05em; +} + +.settings-field input:not([type="checkbox"]):not([type="radio"]), +.settings-field select { + width: 100%; + max-width: 100%; + padding: 10px 14px; + background: var(--bg3); + border: 1px solid var(--bdr); + font-size: .875rem; + color: var(--txt); + outline: none; + transition: border-color .2s; + box-sizing: border-box; +} + +.settings-field input[type="checkbox"], +.settings-field input[type="radio"] { + width: auto; + height: auto; +} + +.settings-field select { + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 4.5 6 7.5 9 4.5'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 32px; +} + +.settings-field input:focus, +.settings-field select:focus { + border-color: var(--acc); +} + +.settings-field input[type="password"] { + letter-spacing: .15em; +} + +.settings-field-inline { + display: flex; + align-items: center; + gap: 8px; +} + +.settings-field-inline input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--acc); +} + +.settings-field-inline label { + font-size: .8125rem; + color: var(--txt2); + text-transform: none; + letter-spacing: 0; +} + +.settings-hint { + font-size: .75rem; + color: var(--txt3); + margin-top: 4px; +} + +.settings-btn-row { + display: flex; + gap: 12px; + margin-top: 8px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Vector Settings + ═══════════════════════════════════════════════════════════════════════════ */ + +.engine-selector { + display: flex; + gap: 16px; + margin-top: 8px; +} + +.engine-option { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: .875rem; + color: var(--txt2); +} + +.engine-option input { + accent-color: var(--hl); + width: 18px; + height: 18px; + margin: 0; + cursor: pointer; +} + +.engine-area { + margin-top: 12px; + padding: 16px; + background: var(--bg3); + border: 1px solid var(--bdr); +} + +.engine-card { + text-align: center; +} + +.engine-card-title { + font-size: 1rem; + font-weight: 600; + margin-bottom: 4px; +} + +.engine-status-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-top: 12px; +} + +.engine-status { + display: flex; + align-items: center; + gap: 6px; + font-size: .8125rem; + color: var(--txt3); + flex: 1; + /* 占 1/3 */ +} + +.engine-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + flex: 2; + /* 占 2/3 */ +} + +/* 针对在线测试连接按钮的特殊处理 */ +#btn-test-vector-api { + flex: 2; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--txt3); +} + +.status-dot.ready { + background: var(--success); +} + +.status-dot.cached { + background: var(--info); +} + +.status-dot.downloading { + background: var(--downloading); + animation: pulse 1s infinite; +} + +.status-dot.error { + background: var(--error); +} + +.status-dot.success { + background: var(--success); +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: .5; + } +} + +.engine-progress { + margin: 12px 0; +} + +.progress-bar { + height: 6px; + background: var(--bdr); + border-radius: 3px; + overflow: hidden; +} + +.progress-inner { + height: 100%; + background: linear-gradient(90deg, var(--hl), var(--hl2)); + border-radius: 3px; + width: 0%; + transition: width .3s; +} + +.progress-text { + font-size: .75rem; + color: var(--txt3); + display: block; + text-align: center; + margin-top: 4px; +} + +.engine-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + flex: 2; +} + +.model-select-row { + display: flex; + gap: 8px; + align-items: center; + margin-bottom: 12px; +} + +.model-select-row select { + flex: 1; + padding: 8px 12px; + background: var(--bg2); + border: 1px solid var(--bdr); + font-size: .875rem; + color: var(--txt); +} + +.model-desc { + font-size: .75rem; + color: var(--txt3); + text-align: left; + margin-bottom: 4px; +} + +.vector-stats { + display: flex; + gap: 8px; + font-size: .875rem; + color: var(--txt2); + margin-top: 8px; +} + +.vector-stats strong { + color: var(--hl); +} + +.vector-mismatch-warning { + font-size: .75rem; + color: var(--downloading); + margin-top: 6px; +} + +.vector-chat-section { + border-top: 1px solid var(--bdr); + padding-top: 16px; + margin-top: 16px; +} + +#vector-action-row { + display: flex; + gap: 8px; + justify-content: center; + width: 100%; +} + +#vector-action-row .btn { + flex: 1; + min-width: 0; +} + +.provider-hint { + font-size: .75rem; + color: var(--txt3); + margin-top: 4px; +} + +.provider-hint a { + color: var(--hl); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Recall Log + ═══════════════════════════════════════════════════════════════════════════ */ + +#recall-log-modal .modal-box { + max-width: 900px; + display: flex; + flex-direction: column; +} + +#recall-log-modal .modal-body { + flex: 1; + min-height: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +#recall-log-content { + font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; + font-size: 12px; + line-height: 1.6; + color: var(--code-txt); + white-space: pre-wrap !important; + overflow-x: hidden !important; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.recall-empty { + color: var(--muted); + text-align: center; + padding: 40px; + font-style: italic; + font-size: .8125rem; + line-height: 1.8; +} + +/* 移动端适配 */ +@media (max-width: 768px) { + #recall-log-modal .modal-box { + max-width: 100%; + max-height: 100%; + height: 100%; + border-radius: 0; + } + + .debug-log-viewer, + #recall-log-content { + font-size: 11px; + padding: 12px; + line-height: 1.5; + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + HF Guide + ═══════════════════════════════════════════════════════════════════════════ */ + +.hf-guide { + font-size: .875rem; + line-height: 1.7; +} + +.hf-section { + margin-bottom: 28px; + padding-bottom: 24px; + border-bottom: 1px solid var(--bdr2); +} + +.hf-section:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.hf-intro { + background: linear-gradient(135deg, rgba(102, 126, 234, .08), rgba(118, 75, 162, .08)); + border: 1px solid rgba(102, 126, 234, .2); + border-radius: 8px; + padding: 20px; + text-align: center; + border-bottom: none; +} + +.hf-intro-text { + font-size: 1.1rem; + margin-bottom: 12px; +} + +.hf-intro-badges { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} + +.hf-badge { + padding: 4px 12px; + background: var(--bg2); + border: 1px solid var(--bdr); + border-radius: 20px; + font-size: .75rem; + color: var(--txt2); +} + +.hf-step-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.hf-step-num { + width: 28px; + height: 28px; + background: var(--acc); + color: var(--inv); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: .875rem; + flex-shrink: 0; +} + +.hf-step-title { + font-size: 1rem; + font-weight: 600; + color: var(--txt); +} + +.hf-step-content { + padding-left: 40px; +} + +.hf-step-content p { + margin: 0 0 12px; +} + +.hf-step-content a { + color: var(--hl); + text-decoration: none; +} + +.hf-step-content a:hover { + text-decoration: underline; +} + +.hf-checklist { + margin: 12px 0; + padding-left: 20px; +} + +.hf-checklist li { + margin-bottom: 6px; +} + +.hf-checklist li::marker { + color: var(--hl); +} + +.hf-checklist code, +.hf-faq code { + background: var(--bg3); + padding: 2px 6px; + border-radius: 3px; + font-size: .8125rem; +} + +.hf-file { + margin-bottom: 16px; + border: 1px solid var(--bdr); + border-radius: 6px; + overflow: hidden; +} + +.hf-file:last-child { + margin-bottom: 0; +} + +.hf-file-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: var(--bg3); + border-bottom: 1px solid var(--bdr); + font-size: .8125rem; +} + +.hf-file-icon { + font-size: 1rem; +} + +.hf-file-name { + font-weight: 600; + font-family: 'SF Mono', Monaco, Consolas, monospace; +} + +.hf-file-note { + color: var(--txt3); + font-size: .75rem; + margin-left: auto; +} + +.hf-code { + margin: 0; + padding: 14px; + background: var(--code-bg); + overflow-x: auto; + position: relative; +} + +.hf-code code { + font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; + font-size: .75rem; + line-height: 1.5; + color: var(--code-txt); + display: block; + white-space: pre; +} + +.hf-code .copy-btn { + position: absolute; + right: 8px; + top: 8px; + padding: 4px 10px; + background: rgba(255, 255, 255, .1); + border: 1px solid rgba(255, 255, 255, .2); + color: var(--muted); + font-size: .6875rem; + cursor: pointer; + border-radius: 4px; + transition: all .2s; +} + +.hf-code .copy-btn:hover { + background: rgba(255, 255, 255, .2); + color: var(--inv); +} + +.hf-status-badge { + display: inline-block; + padding: 2px 10px; + background: rgba(34, 197, 94, .15); + color: var(--success); + border-radius: 10px; + font-size: .75rem; + font-weight: 500; +} + +.hf-config-table { + background: var(--bg3); + border: 1px solid var(--bdr); + border-radius: 6px; + overflow: hidden; +} + +.hf-config-row { + display: flex; + padding: 12px 16px; + border-bottom: 1px solid var(--bdr); +} + +.hf-config-row:last-child { + border-bottom: none; +} + +.hf-config-label { + width: 100px; + flex-shrink: 0; + font-weight: 500; + color: var(--txt2); +} + +.hf-config-value { + flex: 1; + color: var(--txt); +} + +.hf-config-value code { + background: var(--bg2); + padding: 2px 6px; + border-radius: 3px; + font-size: .8125rem; + word-break: break-all; +} + +.hf-faq { + background: var(--bg3); + border: 1px solid var(--bdr); + border-radius: 6px; + padding: 16px 20px; + border-bottom: none; +} + +.hf-faq-title { + font-weight: 600; + margin-bottom: 12px; + color: var(--txt); +} + +.hf-faq ul { + margin: 0; + padding-left: 20px; +} + +.hf-faq li { + margin-bottom: 8px; + color: var(--txt2); +} + +.hf-faq li:last-child { + margin-bottom: 0; +} + +.hf-faq a { + color: var(--hl); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Utilities + ═══════════════════════════════════════════════════════════════════════════ */ + +.hidden { + display: none !important; +} + +.empty { + text-align: center; + padding: 40px; + color: var(--txt3); + font-size: .875rem; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Responsive - Tablet + ═══════════════════════════════════════════════════════════════════════════ */ + +@media (max-width: 1200px) { + .container { + padding: 16px 24px; + } + + main { + grid-template-columns: 1fr; + } + + .right { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + } + + .relations, + .world-state, + .profile { + min-height: 280px; + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Responsive - Mobile + ═══════════════════════════════════════════════════════════════════════════ */ + +@media (max-width: 768px) { + .container { + height: auto; + min-height: 100vh; + padding: 16px; + } + + header { + flex-direction: column; + gap: 16px; + padding-bottom: 16px; + margin-bottom: 16px; + } + + h1 { + font-size: 1.5rem; + } + + .stats { + width: 100%; + justify-content: space-between; + gap: 16px; + text-align: center; + } + + .stat-val { + font-size: 1.75rem; + } + + .stat-lbl { + font-size: .625rem; + } + + .controls { + flex-wrap: wrap; + gap: 8px; + padding: 10px 0; + margin-bottom: 16px; + } + + .spacer { + display: none; + } + + .chk-label { + width: 100%; + justify-content: center; + } + + .btn-group { + width: 100%; + display: flex; + gap: 6px; + } + + .btn-group .btn { + padding: 10px 8px; + font-size: .75rem; + } + + .btn-group .btn-icon { + padding: 10px 8px; + justify-content: center; + } + + .btn-group .btn-icon svg { + width: 14px; + height: 14px; + } + + .btn-group .btn-icon span { + display: none; + } + + main { + display: flex; + flex-direction: column; + gap: 16px; + } + + .left, + .right { + gap: 16px; + } + + .right { + display: flex; + flex-direction: column; + } + + .timeline { + max-height: 400px; + } + + /* 关键:relations 和 profile 完全一致的高度 */ + .relations, + .profile { + min-height: 350px; + max-height: 350px; + height: 350px; + } + + #relation-chart { + height: 100%; + min-height: 300px; + } + + .world-state { + min-height: 180px; + max-height: 180px; + } + + .card { + padding: 16px; + } + + .keywords { + gap: 8px; + margin-top: 12px; + } + + .tag { + padding: 6px 14px; + font-size: .8125rem; + } + + .tl-item { + padding-left: 24px; + padding-bottom: 24px; + } + + .tl-title { + font-size: .9375rem; + } + + .tl-brief { + font-size: .8125rem; + line-height: 1.6; + } + + .modal-box { + max-width: 100%; + max-height: 100%; + height: 100%; + border: none; + } + + .modal-head, + .modal-body, + .modal-foot { + padding: 16px; + } + + .settings-row { + flex-direction: column; + gap: 12px; + } + + .settings-field { + min-width: 100%; + } + + .settings-field input, + .settings-field select { + padding: 12px 14px; + font-size: 1rem; + } + + .fullscreen .modal-box { + width: 100%; + height: 100%; + border-radius: 0; + } + + .hf-step-content { + padding-left: 0; + margin-top: 12px; + } + + .hf-config-row { + flex-direction: column; + gap: 4px; + } + + .hf-config-label { + width: auto; + font-size: .75rem; + color: var(--txt3); + } + + .hf-intro-badges { + gap: 8px; + } + + .hf-badge { + font-size: .6875rem; + padding: 3px 10px; + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Responsive - Small Mobile + ═══════════════════════════════════════════════════════════════════════════ */ + +@media (max-width: 480px) { + .container { + padding: 12px; + } + + header { + padding-bottom: 12px; + margin-bottom: 12px; + } + + h1 { + font-size: 1.25rem; + } + + .subtitle { + font-size: .6875rem; + } + + .stats { + gap: 8px; + } + + .stat { + flex: 1; + } + + .stat-val { + font-size: 1.5rem; + } + + .controls { + gap: 6px; + padding: 8px 0; + margin-bottom: 12px; + } + + .btn-group { + gap: 4px; + } + + .btn-group .btn { + padding: 10px 6px; + font-size: .6875rem; + } + + main, + .left, + .right { + gap: 12px; + } + + .card { + padding: 12px; + } + + .sec-title { + font-size: .6875rem; + } + + .sec-btn { + font-size: .625rem; + padding: 3px 8px; + } + + /* 小屏也保持一致 */ + .relations, + .profile { + min-height: 300px; + max-height: 300px; + height: 300px; + } + + #relation-chart { + height: 100%; + min-height: 250px; + } + + .world-state { + min-height: 150px; + max-height: 150px; + } + + .keywords { + gap: 6px; + margin-top: 10px; + } + + .tag { + padding: 5px 10px; + font-size: .75rem; + } + + .tl-item { + padding-left: 20px; + padding-bottom: 20px; + margin-left: 6px; + } + + .tl-dot { + width: 7px; + height: 7px; + left: -4px; + } + + .tl-head { + flex-direction: column; + align-items: flex-start; + gap: 2px; + } + + .tl-title { + font-size: .875rem; + } + + .tl-time { + font-size: .6875rem; + } + + .tl-brief { + font-size: .8rem; + margin-bottom: 8px; + } + + .tl-meta { + flex-direction: column; + gap: 4px; + font-size: .6875rem; + } + + .modal-head h2 { + font-size: .875rem; + } + + .settings-section-title { + font-size: .625rem; + } + + .settings-field label { + font-size: .6875rem; + } + + .settings-field-inline label { + font-size: .75rem; + } + + .settings-hint { + font-size: .6875rem; + } + + .btn-sm { + padding: 10px 14px; + font-size: .75rem; + width: 100%; + } + + .editor-ta { + min-height: 200px; + font-size: .75rem; + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Touch Devices + ═══════════════════════════════════════════════════════════════════════════ */ + +@media (hover: none) and (pointer: coarse) { + .btn { + min-height: 44px; + } + + .tag { + min-height: 36px; + display: flex; + align-items: center; + } + + .tag:hover { + transform: none; + } + + .tl-item:hover .tl-dot { + transform: none; + } + + .modal-close { + width: 44px; + height: 44px; + } + + .settings-field input, + .settings-field select { + min-height: 44px; + } + + .settings-field-inline input[type="checkbox"] { + width: 22px; + height: 22px; + } + + .sec-btn { + min-height: 32px; + padding: 6px 12px; + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + World State (L3) + ═══════════════════════════════════════════════════════════════════════════ */ + +.world-state { + flex: 0 0 auto; +} + +.world-state-list { + max-height: 200px; + overflow-y: auto; + padding-right: 4px; +} + +.world-group { + margin-bottom: 16px; +} + +.world-group:last-child { + margin-bottom: 0; +} + +.world-group-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--txt3); + margin-bottom: 8px; + padding-bottom: 6px; + border-bottom: 1px solid var(--bdr2); +} + +.world-item { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 8px 10px; + margin-bottom: 6px; + background: var(--bg3); + border: 1px solid var(--bdr2); + border-radius: 6px; + font-size: 0.8125rem; + transition: all 0.15s ease; +} + +.world-item:hover { + border-color: var(--bdr); + background: var(--bg2); +} + +.world-item:last-child { + margin-bottom: 0; +} + +.world-topic { + font-weight: 600; + color: var(--txt); + white-space: nowrap; + flex-shrink: 0; +} + +.world-topic::after { + content: ''; +} + +.world-content { + color: var(--txt2); + flex: 1; + line-height: 1.5; +} + +/* 分类图标颜色 */ +.world-group[data-category="status"] .world-group-title { + color: var(--cat-status); +} + +.world-group[data-category="inventory"] .world-group-title { + color: var(--cat-inventory); +} + +.world-group[data-category="relation"] .world-group-title { + color: var(--cat-relation); +} + +.world-group[data-category="knowledge"] .world-group-title { + color: var(--cat-knowledge); +} + +.world-group[data-category="rule"] .world-group-title { + color: var(--cat-rule); +} + +/* 空状态 */ +.world-state-list .empty { + padding: 24px; + font-size: 0.8125rem; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .world-state { + max-height: none; + } + + .world-state-list { + max-height: 180px; + } + + .world-item { + flex-direction: column; + gap: 4px; + padding: 8px; + } + + .world-topic::after { + content: ''; + } +} + +@media (max-width: 480px) { + .world-state-list { + max-height: 150px; + } + + .world-group-title { + font-size: 0.625rem; + } + + .world-item { + font-size: 0.75rem; + padding: 6px 8px; + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + New Settings Styles + ═══════════════════════════════════════════════════════════════════════════ */ + +.settings-modal-box { + max-width: 680px; +} + +/* Collapsible Section */ +.settings-collapse { + margin-top: 20px; + border-radius: 8px; + overflow: hidden; +} + +.settings-collapse-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + cursor: pointer; + font-size: .8125rem; + font-weight: 500; + color: var(--txt2); + transition: all .2s; + background: var(--bg3); + border: 2px solid var(--bdr); + border-radius: 3px; +} + +.collapse-icon { + width: 16px; + height: 16px; + transition: transform .2s; +} + +.settings-collapse.open .collapse-icon { + transform: rotate(180deg); +} + +.settings-collapse-content { + padding: 16px; + border-top: 1px solid var(--bdr); +} + +/* Checkbox Group */ +.settings-checkbox-group { + margin-bottom: 20px; + padding: 0; + background: transparent; + border: none; +} + +.settings-checkbox-group:last-child { + margin-bottom: 0; +} + +.settings-checkbox { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + user-select: none; +} + +.settings-checkbox input[type="checkbox"] { + display: none; +} + +.checkbox-mark { + width: 20px; + height: 20px; + border: 2px solid var(--bdr); + border-radius: 4px; + background: var(--bg2); + position: relative; + transition: all .2s; + flex-shrink: 0; +} + +.settings-checkbox input:checked+.checkbox-mark { + background: var(--acc); + border-color: var(--acc); +} + +.settings-checkbox input:checked+.checkbox-mark::after { + content: ''; + position: absolute; + left: 6px; + top: 2px; + width: 5px; + height: 10px; + border: solid var(--inv); + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.checkbox-label { + font-size: .875rem; + color: var(--txt); +} + +.settings-checkbox-group .settings-hint { + margin-left: 30px; + margin-top: 4px; +} + +/* Sub Options */ +.settings-sub-options { + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed var(--bdr); +} + +/* Filter Rules */ +.filter-rules-section { + margin-top: 20px; + padding: 16px; + background: var(--bg3); + border: 1px solid var(--bdr); + border-radius: 8px; +} + +.filter-rules-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + gap: 12px; +} + +.filter-rules-header label { + font-size: .75rem; + color: var(--txt3); + text-transform: uppercase; + letter-spacing: .05em; + font-weight: 600; + flex: 1; + /* 1/3 */ +} + +.btn-add { + flex: 2; + /* 2/3 */ + justify-content: center; + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; +} + +.filter-rules-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; +} + +.filter-rule-item { + display: flex; + gap: 8px; + align-items: flex-start; + padding: 10px 12px; + background: var(--bg2); + border: 1px solid var(--bdr2); + border-radius: 6px; +} + +.filter-rule-inputs { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + flex: 1; +} + +.filter-rule-item input { + width: 100%; + padding: 8px 10px; + background: var(--bg3); + border: 1px solid var(--bdr); + font-size: .8125rem; + color: var(--txt); + border-radius: 4px; +} + +.filter-rule-item input:focus { + border-color: var(--acc); + outline: none; +} + +.filter-rule-item .rule-arrow { + color: var(--txt3); + font-size: .875rem; + flex-shrink: 0; + padding: 2px 0; +} + +.filter-rule-item .btn-del-rule { + padding: 6px 10px; + background: transparent; + border: 1px solid var(--hl); + color: var(--hl); + cursor: pointer; + border-radius: 4px; + font-size: .75rem; + transition: all .2s; + flex-shrink: 0; + align-self: center; +} + +.filter-rule-item .btn-del-rule:hover { + background: var(--hl-soft); +} + +/* Vector Stats - Original horizontal layout */ +.vector-stats { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 16px; + font-size: .875rem; + color: var(--txt2); + margin-top: 8px; +} + +.vector-stat-col { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.vector-stat-label { + font-size: .75rem; + color: var(--txt3); +} + +.vector-stat-value { + color: var(--txt2); +} + +.vector-stat-value strong { + color: var(--hl); +} + +.vector-stat-sep { + color: var(--txt3); + align-self: center; +} + +.vector-io-section { + border-top: 1px solid var(--bdr); + padding-top: 16px; + margin-top: 16px; +} + +/* Mobile Settings Responsive */ +@media (max-width: 768px) { + .settings-modal-box { + max-width: 100%; + } + + .settings-collapse-header { + padding: 14px 16px; + } + + .settings-checkbox-group { + padding: 14px; + } + + .checkbox-label { + font-size: .8125rem; + } + + .vector-stats { + gap: 8px; + } + + .vector-stat-sep { + display: none; + } + + .vector-stat-col { + flex-direction: row; + gap: 4px; + } + + .settings-field { + min-width: 100px; + } +} + +@media (max-width: 480px) { + .settings-checkbox-group { + padding: 12px; + } + + .checkbox-mark { + width: 18px; + height: 18px; + } + + .settings-checkbox input:checked+.checkbox-mark::after { + left: 5px; + top: 1px; + width: 4px; + height: 9px; + } + + .filter-rules-section { + padding: 12px; + } + + .filter-rule-item { + padding: 8px 10px; + } + + .filter-rule-item .btn-del-rule { + padding: 4px 8px; + } + + .settings-sub-options .settings-row { + flex-direction: column; + } +} + +/* Settings Tabs */ +.settings-tabs { + display: flex; + gap: 24px; + align-self: flex-end; + /* 使底部边框与 header 底部对齐 */ + margin-bottom: -20px; + /* 抵消 modal-head 的 padding,让边框贴合底部 */ +} + +.settings-tab { + font-size: .875rem; + color: var(--txt3); + cursor: pointer; + padding-bottom: 20px; + /* 增加内边距使点击区域更大且贴合底部 */ + border-bottom: 2px solid transparent; + transition: all .2s; + user-select: none; + text-transform: uppercase; + letter-spacing: .1em; + font-weight: 500; +} + +.settings-tab:hover { + color: var(--txt); +} + +.settings-tab.active { + color: var(--hl); + border-bottom-color: var(--hl); + font-weight: 600; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; + animation: fadeIn .3s ease; +} + +.debug-log-header { + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px dashed var(--bdr2); +} + +.debug-title { + font-size: .875rem; + font-weight: 600; + color: var(--txt); + margin-bottom: 4px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Recall Log / Debug Log + ═══════════════════════════════════════════════════════════════════════════ */ + +.debug-log-viewer { + background: var(--code-bg); + color: var(--code-txt); + padding: 16px; + border-radius: 8px; + font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; + font-size: 12px; + line-height: 1.6; + max-height: 60vh; + overflow-y: auto; + overflow-x: hidden; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: break-word; +} + +.recall-empty { + color: var(--muted); + text-align: center; + padding: 40px; + font-style: italic; + font-size: .8125rem; + line-height: 1.8; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 记忆锚点区域(L0) + ═══════════════════════════════════════════════════════════════════════════ */ + + + +/* ═══════════════════════════════════════════════════════════════════════════ + Metrics Log Styling + ═══════════════════════════════════════════════════════════════════════════ */ + +#recall-log-content .metric-warn { + color: var(--downloading); +} + +#recall-log-content .metric-error { + color: var(--error); +} + +#recall-log-content .metric-good { + color: var(--success); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Guide Tab (使用说明) + ═══════════════════════════════════════════════════════════════════════════ */ + +.guide-container { + font-size: .875rem; + line-height: 1.7; + color: var(--txt2); +} + +/* Section */ +.guide-section { + margin-bottom: 28px; + padding-bottom: 24px; + border-bottom: 1px solid var(--bdr2); +} + +.guide-section-last, +.guide-section:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +/* Title */ +.guide-title { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 14px; + font-size: 1rem; + font-weight: 600; + color: var(--txt); +} + +.guide-num { + width: 26px; + height: 26px; + background: var(--acc); + color: var(--inv); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: .8125rem; + flex-shrink: 0; +} + +/* Text */ +.guide-text { + margin-bottom: 8px; + color: var(--txt2); +} + +.guide-text:last-child { + margin-bottom: 0; +} + +.guide-text a { + color: var(--hl); + text-decoration: none; +} + +.guide-text a:hover { + text-decoration: underline; +} + +/* Steps */ +.guide-steps { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 4px; +} + +.guide-step { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.guide-step-num { + width: 22px; + height: 22px; + background: var(--hl); + color: var(--inv); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: .75rem; + flex-shrink: 0; + margin-top: 2px; +} + +.guide-step-body { + flex: 1; + min-width: 0; +} + +.guide-step-title { + font-weight: 600; + color: var(--txt); + margin-bottom: 2px; + font-size: .875rem; +} + +.guide-step-desc { + color: var(--txt2); + font-size: .8125rem; + line-height: 1.6; +} + +.guide-step-desc a { + color: var(--hl); + text-decoration: none; +} + +.guide-step-desc a:hover { + text-decoration: underline; +} + +.guide-tag { + display: inline-block; + padding: 1px 8px; + background: var(--hl-soft); + color: var(--hl); + border-radius: 10px; + font-size: .6875rem; + font-weight: 500; + vertical-align: middle; + margin-left: 4px; +} + +/* Card List */ +.guide-card-list { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-top: 4px; +} + +.guide-card { + padding: 14px 16px; + background: var(--bg3); + border: 1px solid var(--bdr2); + border-radius: 6px; + transition: border-color .2s; +} + +.guide-card:hover { + border-color: var(--bdr); +} + +.guide-card-title { + font-size: .8125rem; + font-weight: 600; + color: var(--txt); + margin-bottom: 6px; +} + +.guide-card-desc { + font-size: .75rem; + color: var(--txt3); + line-height: 1.6; +} + +/* Highlight */ +.guide-highlight { + padding: 16px 20px; + background: linear-gradient(135deg, rgba(216, 122, 122, .06), rgba(216, 122, 122, .02)); + border: 1px solid rgba(216, 122, 122, .2); + border-radius: 8px; + margin-top: 4px; +} + +.guide-highlight-title { + font-weight: 600; + color: var(--txt); + margin-bottom: 8px; + font-size: .875rem; +} + +/* List */ +.guide-list { + margin: 8px 0; + padding-left: 20px; + color: var(--txt2); +} + +.guide-list li { + margin-bottom: 6px; + line-height: 1.6; +} + +.guide-list li:last-child { + margin-bottom: 0; +} + +.guide-list li::marker { + color: var(--hl); +} + +.guide-list a { + color: var(--hl); + text-decoration: none; +} + +.guide-list a:hover { + text-decoration: underline; +} + +.guide-list code { + background: var(--bg3); + padding: 1px 5px; + border-radius: 3px; + font-size: .75rem; + font-family: 'SF Mono', Monaco, Consolas, monospace; + color: var(--txt); +} + +.guide-list-inner { + margin-top: 6px; + margin-bottom: 4px; + padding-left: 18px; +} + +.guide-list-inner li { + margin-bottom: 3px; + font-size: .8125rem; + color: var(--txt3); +} + +.guide-list-inner li::marker { + color: var(--txt3); +} + +/* Tip */ +.guide-tip { + display: flex; + gap: 10px; + align-items: flex-start; + padding: 12px 16px; + background: var(--bg3); + border: 1px solid var(--bdr2); + border-left: 3px solid var(--hl); + border-radius: 0 6px 6px 0; + margin-top: 12px; +} + +.guide-tip-icon { + font-size: 1rem; + flex-shrink: 0; + line-height: 1.5; +} + +.guide-tip-text { + font-size: .8125rem; + color: var(--txt2); + line-height: 1.6; + flex: 1; +} + +/* Tips List */ +.guide-tips-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 4px; +} + +.guide-tips-list .guide-tip { + margin-top: 0; +} + +/* FAQ */ +.guide-faq-list { + display: flex; + flex-direction: column; + gap: 0; + margin-top: 4px; +} + +.guide-faq-item { + padding: 14px 0; + border-bottom: 1px solid var(--bdr2); +} + +.guide-faq-item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.guide-faq-item:first-child { + padding-top: 0; +} + +.guide-faq-q { + font-weight: 600; + color: var(--txt); + font-size: .8125rem; + margin-bottom: 6px; + display: flex; + align-items: center; + gap: 6px; +} + +.guide-faq-q::before { + content: 'Q'; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + background: var(--acc); + color: var(--inv); + border-radius: 3px; + font-size: .625rem; + font-weight: 700; + flex-shrink: 0; +} + +.guide-faq-a { + font-size: .8125rem; + color: var(--txt2); + line-height: 1.6; + padding-left: 24px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Guide Tab - Responsive + ═══════════════════════════════════════════════════════════════════════════ */ + +@media (max-width: 768px) { + .guide-container { + font-size: .8125rem; + } + + .guide-title { + font-size: .9375rem; + } + + .guide-num { + width: 24px; + height: 24px; + font-size: .75rem; + } + + .guide-section { + margin-bottom: 20px; + padding-bottom: 18px; + } + + .guide-card-list { + grid-template-columns: 1fr; + gap: 8px; + } + + .guide-card { + padding: 12px 14px; + } + + .guide-step { + gap: 10px; + } + + .guide-step-num { + width: 20px; + height: 20px; + font-size: .6875rem; + } + + .guide-highlight { + padding: 14px 16px; + } + + .guide-tip { + padding: 10px 12px; + } + + .guide-faq-a { + padding-left: 0; + } + + .guide-faq-q::before { + width: 16px; + height: 16px; + font-size: .5625rem; + } +} + +@media (max-width: 480px) { + .guide-container { + font-size: .75rem; + } + + .guide-title { + font-size: .875rem; + gap: 8px; + } + + .guide-num { + width: 22px; + height: 22px; + font-size: .6875rem; + } + + .guide-section { + margin-bottom: 16px; + padding-bottom: 14px; + } + + .guide-steps { + gap: 12px; + } + + .guide-step-title { + font-size: .8125rem; + } + + .guide-step-desc { + font-size: .75rem; + } + + .guide-card-title { + font-size: .75rem; + } + + .guide-card-desc { + font-size: .6875rem; + } + + .guide-highlight { + padding: 12px 14px; + } + + .guide-highlight-title { + font-size: .8125rem; + } + + .guide-list { + padding-left: 16px; + font-size: .75rem; + } + + .guide-list-inner li { + font-size: .75rem; + } + + .guide-tip { + padding: 8px 10px; + gap: 8px; + } + + .guide-tip-icon { + font-size: .875rem; + } + + .guide-tip-text { + font-size: .75rem; + } + + .guide-faq-q { + font-size: .75rem; + } + + .guide-faq-a { + font-size: .75rem; + } + + .guide-faq-item { + padding: 10px 0; + } +} + +@media (hover: none) and (pointer: coarse) { + .guide-card:hover { + border-color: var(--bdr2); + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Neo-Brutalism UI Styles + ═══════════════════════════════════════════════════════════════════════════ */ + +.neo-card { + background: var(--bg2); + border: 2px solid var(--bdr); + border-radius: 8px; + box-shadow: 4px 4px 0 var(--bdr); + padding: 16px; + margin-bottom: 24px; + transition: transform 0.2s, box-shadow 0.2s; + /* Ensure it doesn't overlap with flow */ + position: relative; +} + +.neo-card:hover { + transform: translate(1px, 1px); + box-shadow: 3px 3px 0 var(--bdr); +} + +.neo-card-title { + font-size: 0.875rem; + font-weight: 700; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 10px; + color: var(--txt); + padding-bottom: 8px; + border-bottom: 2px solid var(--bdr); +} + +.neo-badge { + /* Explicitly requested Black Background & White Text */ + background: var(--acc); + color: var(--inv); + padding: 2px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.05em; + display: inline-block; +} + +/* Specific tweaks for neo-card content */ +.neo-card .settings-row { + margin-bottom: 12px; +} + +.neo-card .vector-stats { + background: var(--bg3); + border: 1px solid var(--bdr); + padding: 10px; + border-radius: 6px; +} + +.neo-card .settings-hint { + color: var(--txt2); +} + +/* Tools section styling */ +.neo-tools-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 32px; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 2px dashed var(--bdr); + color: var(--txt3); + font-weight: 600; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.1em; +} diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index e0d31db..bf666ef 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -1,3 +1,4 @@ + @@ -6,1391 +7,15 @@ 剧情总结 · Story Summary - +
+
-

剧情总结

+

剧情总结

Story Summary · Timeline · Character Arcs
@@ -1409,59 +34,87 @@
+ +
- - - +
+ + + +
+ +
+
-
核心关键词
+
核心关键词
+
+ +
-
剧情时间线
+
剧情时间线
+
+
+ +
+
+
世界状态
+ +
+
+
+ +
人物关系
- + +
+ +
人物档案
-
选择角色 +
+ 选择角色
暂无角色
@@ -1476,15 +129,18 @@
+ +