diff --git a/addon/chrome/content/preferences.xhtml b/addon/chrome/content/preferences.xhtml
index af67264..8b13789 100644
--- a/addon/chrome/content/preferences.xhtml
+++ b/addon/chrome/content/preferences.xhtml
@@ -1,36 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/addon/chrome/content/zoteroPane.css b/addon/chrome/content/zoteroPane.css
deleted file mode 100644
index 8c95c1e..0000000
--- a/addon/chrome/content/zoteroPane.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.makeItRed {
- background-color: tomato;
-}
diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl
index 892d267..04c1f65 100644
--- a/addon/locale/en-US/addon.ftl
+++ b/addon/locale/en-US/addon.ftl
@@ -1,11 +1,5 @@
-startup-begin = Addon is loading
-startup-finish = Addon is ready
-menuitem-label = Addon Template: Helper Examples
-menupopup-label = Addon Template: Menupopup
-menuitem-submenulabel = Addon Template
-menuitem-filemenulabel = Addon Template: File Menuitem
-prefs-title = Template
-prefs-table-title = Title
-prefs-table-detail = Detail
-tabpanel-lib-tab-label = Lib Tab
-tabpanel-reader-tab-label = Reader Tab
\ No newline at end of file
+menuitem-updatetldrlabel = update TLDR
+menucollection-updatetldrlabel = update TLDR
+itembox-tldrlabel = TLDR
+tldr-unrelated = TLDR Unrelated in Semantic scholar
+tldr-itemnotfound = Item Not Found in Semantic scholar
\ No newline at end of file
diff --git a/addon/locale/zh-CN/addon.ftl b/addon/locale/zh-CN/addon.ftl
index 16d97d3..0882e0c 100644
--- a/addon/locale/zh-CN/addon.ftl
+++ b/addon/locale/zh-CN/addon.ftl
@@ -1,11 +1,4 @@
-startup-begin = 插件加载中
-startup-finish = 插件已就绪
-menuitem-label = 插件模板: 帮助工具样例
-menupopup-label = 插件模板: 弹出菜单
-menuitem-submenulabel = 插件模板:子菜单
-menuitem-filemenulabel = 插件模板: 文件菜单
-prefs-title = 插件模板
-prefs-table-title = 标题
-prefs-table-detail = 详情
-tabpanel-lib-tab-label = 库标签
-tabpanel-reader-tab-label = 阅读器标签
\ No newline at end of file
+menuitem-updatetldrlabel = 更新TLDR
+menucollection-updatetldrlabel = 批量更新TLDR
+itembox-tldrlabel = TLDR
+prefs-title = 插件模板
\ No newline at end of file
diff --git a/addon/prefs.js b/addon/prefs.js
index be8f47d..beff801 100644
--- a/addon/prefs.js
+++ b/addon/prefs.js
@@ -1,3 +1 @@
-/* eslint-disable no-undef */
-pref("__prefsPrefix__.enable", true);
-pref("__prefsPrefix__.input", "This is input");
+/* eslint-disable no-undef */
\ No newline at end of file
diff --git a/src/addon.ts b/src/addon.ts
index a9a70af..cf2e864 100644
--- a/src/addon.ts
+++ b/src/addon.ts
@@ -14,8 +14,6 @@ class Addon {
};
prefs?: {
window: Window;
- columns: Array;
- rows: Array<{ [dataKey: string]: string }>;
};
dialog?: DialogHelper;
};
diff --git a/src/hooks.ts b/src/hooks.ts
index 7b4efa2..e863bc2 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -1,14 +1,13 @@
import {
- BasicExampleFactory,
- HelperExampleFactory,
- KeyExampleFactory,
- PromptExampleFactory,
- UIExampleFactory,
-} from "./modules/examples";
+ RegisterFactory,
+ UIFactory,
+} from "./modules/Common";
import { config } from "../package.json";
import { getString, initLocale } from "./utils/locale";
import { registerPrefsScripts } from "./modules/preferenceScript";
import { createZToolkit } from "./utils/ztoolkit";
+import { TLDRFetcher, TLDRFieldKey } from "./modules/tldrFetcher";
+import { Data, DataStorage } from "./modules/dataStorage";
async function onStartup() {
await Promise.all([
@@ -18,9 +17,11 @@ async function onStartup() {
]);
initLocale();
- BasicExampleFactory.registerPrefs();
+ await DataStorage.instance(TLDRFieldKey).getAsync(); // 加载TLDR数据
- BasicExampleFactory.registerNotifier();
+ RegisterFactory.registerPrefs();
+
+ RegisterFactory.registerNotifier();
await onMainWindowLoad(window);
}
@@ -29,60 +30,15 @@ async function onMainWindowLoad(win: Window): Promise {
// Create ztoolkit for every window
addon.data.ztoolkit = createZToolkit();
- const popupWin = new ztoolkit.ProgressWindow(config.addonName, {
- closeOnClick: true,
- closeTime: -1,
- })
- .createLine({
- text: getString("startup-begin"),
- type: "default",
- progress: 0,
- })
- .show();
-
- KeyExampleFactory.registerShortcuts();
-
- await Zotero.Promise.delay(1000);
- popupWin.changeLine({
- progress: 30,
- text: `[30%] ${getString("startup-begin")}`,
- });
-
- UIExampleFactory.registerStyleSheet();
-
- UIExampleFactory.registerRightClickMenuItem();
-
- UIExampleFactory.registerRightClickMenuPopup();
-
- UIExampleFactory.registerWindowMenuWithSeparator();
-
- await UIExampleFactory.registerExtraColumn();
-
- await UIExampleFactory.registerExtraColumnWithCustomCell();
-
- await UIExampleFactory.registerCustomCellRenderer();
-
- await UIExampleFactory.registerCustomItemBoxRow();
-
- UIExampleFactory.registerLibraryTabPanel();
-
- await UIExampleFactory.registerReaderTabPanel();
-
- PromptExampleFactory.registerNormalCommandExample();
-
- PromptExampleFactory.registerAnonymousCommandExample();
-
- PromptExampleFactory.registerConditionalCommandExample();
-
await Zotero.Promise.delay(1000);
- popupWin.changeLine({
- progress: 100,
- text: `[100%] ${getString("startup-finish")}`,
- });
- popupWin.startCloseTimer(5000);
+ UIFactory.registerRightClickMenuItem();
- addon.hooks.onDialogEvents("dialogExample");
+ UIFactory.registerRightClickCollectionMenuItem();
+
+ await UIFactory.registerTLDRItemBoxRow();
+
+ onLoad();
}
async function onMainWindowUnload(win: Window): Promise {
@@ -108,16 +64,9 @@ async function onNotify(
ids: Array,
extraData: { [key: string]: any },
) {
- // You can add your code to the corresponding notify type
- ztoolkit.log("notify", event, type, ids, extraData);
- if (
- event == "select" &&
- type == "tab" &&
- extraData[ids[0]].type == "reader"
- ) {
- BasicExampleFactory.exampleNotifierCallback();
- } else {
- return;
+ Zotero.log(`${event} ${type} ${ids}, ${extraData}`);
+ if (event == "add" && type == "item" && ids.length > 0) {
+ onNotifyAddItems(ids);
}
}
@@ -137,42 +86,73 @@ async function onPrefsEvent(type: string, data: { [key: string]: any }) {
}
}
-function onShortcuts(type: string) {
- switch (type) {
- case "larger":
- KeyExampleFactory.exampleShortcutLargerCallback();
- break;
- case "smaller":
- KeyExampleFactory.exampleShortcutSmallerCallback();
- break;
- case "confliction":
- KeyExampleFactory.exampleShortcutConflictingCallback();
- break;
- default:
- break;
- }
+function onLoad() {
+ (async () => {
+ let needFetchItems: Zotero.Item[] = [];
+ for (const lib of Zotero.Libraries.getAll()) {
+ needFetchItems = needFetchItems.concat((await Zotero.Items.getAll(lib.id)).filter((item: Zotero.Item) => {
+ return item.isRegularItem() && !item.isCollection();
+ }));
+ }
+ onUpdateItems(needFetchItems, false);
+ })();
}
-function onDialogEvents(type: string) {
- switch (type) {
- case "dialogExample":
- HelperExampleFactory.dialogExample();
- break;
- case "clipboardExample":
- HelperExampleFactory.clipboardExample();
- break;
- case "filePickerExample":
- HelperExampleFactory.filePickerExample();
- break;
- case "progressWindowExample":
- HelperExampleFactory.progressWindowExample();
- break;
- case "vtableExample":
- HelperExampleFactory.vtableExample();
- break;
- default:
- break;
+function onNotifyAddItems(ids: (string | number)[]) {
+ const addedRegularItems: Zotero.Item[] = [];
+ for (const id of ids) {
+ const item = Zotero.Items.get(id);
+ if (item.isRegularItem()) {
+ addedRegularItems.push(item);
+ }
}
+ (async function () {
+ await Zotero.Promise.delay(3000);
+ onUpdateItems(addedRegularItems, false);
+ })();
+}
+
+function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
+ items = items.filter((item: Zotero.Item) => {
+ if (!item.getField('title')) { return false; }
+ if (!forceFetch) { return DataStorage.instance(TLDRFieldKey).get()[item.id] === undefined; }
+ return true;
+ });
+ if (items.length <= 0) { return; }
+ const newPopWin = (closeOnClick = false) => {
+ return new ztoolkit.ProgressWindow(config.addonName, {
+ closeOnClick: closeOnClick,
+ }).createLine({
+ text: `Waiting: ${items.length}, succeed: 0, failed: 0`,
+ type: "default",
+ progress: 0,
+ });
+ }
+ const popupWin = newPopWin().show(-1);
+ (async function () {
+ const count = items.length;
+ const failedItems: Zotero.Item[] = [];
+ const succeedItems: Zotero.Item[] = [];
+ await (async function () {
+ for (const [index, item] of items.entries()) {
+ await new TLDRFetcher(item).fetchTLDR() ? succeedItems.push(item) : failedItems.push(item);
+ ztoolkit.ItemBox.refresh();
+ popupWin.changeLine({
+ progress: index * 100 / count,
+ text: `Waiting: ${count - index - 1}, succeed: ${succeedItems.length}, failed: ${failedItems.length}`,
+ });
+ }
+ })();
+
+ await (async function () {
+ popupWin.changeLine({
+ type: "success",
+ progress: 100,
+ text: `Success: ${succeedItems.length}\nFailed: ${failedItems.length}`,
+ });
+ popupWin.startCloseTimer(3000);
+ })();
+ })();
}
// Add your hooks here. For element click, etc.
@@ -186,6 +166,5 @@ export default {
onMainWindowUnload,
onNotify,
onPrefsEvent,
- onShortcuts,
- onDialogEvents,
+ onUpdateItems,
};
diff --git a/src/modules/Common.ts b/src/modules/Common.ts
new file mode 100644
index 0000000..565f675
--- /dev/null
+++ b/src/modules/Common.ts
@@ -0,0 +1,121 @@
+import { config } from "../../package.json";
+import { getString } from "../utils/locale";
+import { DataStorage } from "./dataStorage";
+import { TLDRFieldKey, TLDRItemNotFound, TLDRUnrelated } from "./tldrFetcher";
+
+export class RegisterFactory {
+ // 注册zotero的通知
+ static registerNotifier() {
+ const callback = {
+ notify: async (
+ event: string,
+ type: string,
+ ids: number[] | string[],
+ extraData: { [key: string]: any }
+ ) => {
+ if (!addon?.data.alive) {
+ this.unregisterNotifier(notifierID);
+ return;
+ }
+ addon.hooks.onNotify(event, type, ids, extraData);
+ },
+ };
+
+ // Register the callback in Zotero as an item observer
+ const notifierID = Zotero.Notifier.registerObserver(callback, [
+ "item",
+ ]);
+
+ // Unregister callback when the window closes (important to avoid a memory leak)
+ window.addEventListener(
+ "unload",
+ (e: Event) => {
+ this.unregisterNotifier(notifierID);
+ },
+ false
+ );
+ }
+
+ private static unregisterNotifier(notifierID: string) {
+ Zotero.Notifier.unregisterObserver(notifierID);
+ }
+
+
+ // 注册首选项配置
+ static registerPrefs() {
+ // const prefOptions = {
+ // pluginID: config.addonID,
+ // src: rootURI + "chrome/content/preferences.xhtml",
+ // label: getString("prefs.title"),
+ // image: `chrome://${config.addonRef}/content/icons/favicon.png`,
+ // extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`],
+ // defaultXUL: true,
+ // };
+ // ztoolkit.PreferencePane.register(prefOptions);
+ }
+}
+
+export class UIFactory {
+ // item右键菜单
+ static registerRightClickMenuItem() {
+ const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`;
+ // item menuitem with icon
+ ztoolkit.Menu.register("item", {
+ tag: "menuitem",
+ id: "zotero-itemmenu-tldr",
+ label: getString("menuitem-updatetldrlabel"),
+ commandListener: (ev) => {
+ const selectedItems = ZoteroPane.getSelectedItems() ?? [];
+ addon.hooks.onUpdateItems(selectedItems, selectedItems.length <= 1);
+ },
+ icon: menuIcon,
+ });
+ }
+
+ // collection右键菜单
+ static registerRightClickCollectionMenuItem() {
+ const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`;
+ ztoolkit.Menu.register("collection", {
+ tag: "menuitem",
+ id: "zotero-collectionmenu-tldr",
+ label: getString("menucollection-updatetldrlabel"),
+ commandListener: (ev) => addon.hooks.onUpdateItems(ZoteroPane.getSelectedCollection()?.getChildItems() ?? [], false),
+ icon: menuIcon,
+ })
+ }
+
+ // tldr行
+ static async registerTLDRItemBoxRow() {
+ await ztoolkit.ItemBox.register(
+ TLDRFieldKey,
+ getString("itembox-tldrlabel"),
+ (field, unformatted, includeBaseMapped, item, original) => {
+ const tldrInfo = DataStorage.instance(TLDRFieldKey).get()[item.id];
+ if (tldrInfo === TLDRUnrelated) {
+ return getString(TLDRUnrelated);
+ } else if (tldrInfo === TLDRItemNotFound) {
+ return getString(TLDRItemNotFound);
+ } else if (tldrInfo) {
+ return tldrInfo;
+ } else {
+ return "";
+ }
+ },
+ {
+ editable: true,
+ setFieldHook: (field, value, loadIn, item, original) => {
+ (async () => {
+ await DataStorage.instance(TLDRFieldKey).modify((data: any) => {
+ data[item.id] = value;
+ return data;
+ });
+ ztoolkit.ItemBox.refresh();
+ })();
+ return true;
+ },
+ index: 2,
+ multiline: true,
+ },
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/modules/dataStorage.ts b/src/modules/dataStorage.ts
new file mode 100644
index 0000000..de04411
--- /dev/null
+++ b/src/modules/dataStorage.ts
@@ -0,0 +1,80 @@
+import { config } from "../../package.json";
+
+export class Data {
+ private filePath: string
+ private inited = false;
+ private _data: any
+
+ constructor(filePath: string) {
+ this.filePath = filePath;
+ }
+
+ async getAsync() {
+ await this.initDataIfNeed();
+ return this.data;
+ }
+
+ get() {
+ return this.data;
+ }
+
+ async modify(action: (data: any) => Promise) {
+ await this.initDataIfNeed();
+ const data = await this.data;
+ const newData = await action(data);
+ try {
+ await IOUtils.writeJSON(this.filePath, newData, {mode: 'overwrite', compress: false});
+ this.data = newData;
+ return newData;
+ } catch (error) {
+ return data;
+ }
+ }
+
+ async delete() {
+ try {
+ await IOUtils.remove(this.filePath);
+ this.data = {};
+ return true;
+ } catch (error) {
+ return false;
+ }
+ }
+
+ private get data() {
+ return this._data;
+ }
+
+ private set data(value: any) {
+ this._data = value;
+ }
+
+ private async initDataIfNeed() {
+ if (this.inited) { return; }
+ try {
+ this.data = await IOUtils.readJSON(this.filePath, {decompress: false});
+ } catch (error) {
+ this.data = {};
+ }
+ }
+}
+
+export class DataStorage {
+ private readonly dataDir = PathUtils.join(PathUtils.profileDir, 'extensions', config.addonName);
+ private dataMap: { [key: string]: Data } = {};
+
+ private static shared = new DataStorage();
+
+ static instance(dataType: string) {
+ const path = PathUtils.join(this.shared.dataDir, dataType);
+ if (this.shared.dataMap[dataType] === undefined) {
+ const data = new Data(path);
+ this.shared.dataMap[dataType] = data;
+ return data;
+ } else {
+ return this.shared.dataMap[dataType];
+ }
+ }
+
+ private constructor() { }
+}
diff --git a/src/modules/examples.ts b/src/modules/examples.ts
deleted file mode 100644
index 79ea9cd..0000000
--- a/src/modules/examples.ts
+++ /dev/null
@@ -1,979 +0,0 @@
-import { config } from "../../package.json";
-import { getString } from "../utils/locale";
-
-function example(
- target: any,
- propertyKey: string | symbol,
- descriptor: PropertyDescriptor,
-) {
- const original = descriptor.value;
- descriptor.value = function (...args: any) {
- try {
- ztoolkit.log(`Calling example ${target.name}.${String(propertyKey)}`);
- return original.apply(this, args);
- } catch (e) {
- ztoolkit.log(`Error in example ${target.name}.${String(propertyKey)}`, e);
- throw e;
- }
- };
- return descriptor;
-}
-
-export class BasicExampleFactory {
- @example
- static registerNotifier() {
- const callback = {
- notify: async (
- event: string,
- type: string,
- ids: number[] | string[],
- extraData: { [key: string]: any },
- ) => {
- if (!addon?.data.alive) {
- this.unregisterNotifier(notifierID);
- return;
- }
- addon.hooks.onNotify(event, type, ids, extraData);
- },
- };
-
- // Register the callback in Zotero as an item observer
- const notifierID = Zotero.Notifier.registerObserver(callback, [
- "tab",
- "item",
- "file",
- ]);
-
- // Unregister callback when the window closes (important to avoid a memory leak)
- window.addEventListener(
- "unload",
- (e: Event) => {
- this.unregisterNotifier(notifierID);
- },
- false,
- );
- }
-
- @example
- static exampleNotifierCallback() {
- new ztoolkit.ProgressWindow(config.addonName)
- .createLine({
- text: "Open Tab Detected!",
- type: "success",
- progress: 100,
- })
- .show();
- }
-
- @example
- private static unregisterNotifier(notifierID: string) {
- Zotero.Notifier.unregisterObserver(notifierID);
- }
-
- @example
- static registerPrefs() {
- const prefOptions = {
- pluginID: config.addonID,
- src: rootURI + "chrome/content/preferences.xhtml",
- label: getString("prefs-title"),
- image: `chrome://${config.addonRef}/content/icons/favicon.png`,
- defaultXUL: true,
- };
- ztoolkit.PreferencePane.register(prefOptions);
- }
-}
-
-export class KeyExampleFactory {
- @example
- static registerShortcuts() {
- const keysetId = `${config.addonRef}-keyset`;
- const cmdsetId = `${config.addonRef}-cmdset`;
- const cmdSmallerId = `${config.addonRef}-cmd-smaller`;
- // Register an event key for Alt+L
- ztoolkit.Shortcut.register("event", {
- id: `${config.addonRef}-key-larger`,
- key: "L",
- modifiers: "alt",
- callback: (keyOptions) => {
- addon.hooks.onShortcuts("larger");
- },
- });
- // Register an element key using for Alt+S
- ztoolkit.Shortcut.register("element", {
- id: `${config.addonRef}-key-smaller`,
- key: "S",
- modifiers: "alt",
- xulData: {
- document,
- command: cmdSmallerId,
- _parentId: keysetId,
- _commandOptions: {
- id: cmdSmallerId,
- document,
- _parentId: cmdsetId,
- oncommand: `Zotero.${config.addonInstance}.hooks.onShortcuts('smaller')`,
- },
- },
- });
- // Here we register an conflict key for Alt+S
- // just to show how the confliction check works.
- // This is something you should avoid in your plugin.
- ztoolkit.Shortcut.register("event", {
- id: `${config.addonRef}-key-smaller-conflict`,
- key: "S",
- modifiers: "alt",
- callback: (keyOptions) => {
- ztoolkit.getGlobal("alert")("Smaller! This is a conflict key.");
- },
- });
- // Register an event key to check confliction
- ztoolkit.Shortcut.register("event", {
- id: `${config.addonRef}-key-check-conflict`,
- key: "C",
- modifiers: "alt",
- callback: (keyOptions) => {
- addon.hooks.onShortcuts("confliction");
- },
- });
- new ztoolkit.ProgressWindow(config.addonName)
- .createLine({
- text: "Example Shortcuts: Alt+L/S/C",
- type: "success",
- })
- .show();
- }
-
- @example
- static exampleShortcutLargerCallback() {
- new ztoolkit.ProgressWindow(config.addonName)
- .createLine({
- text: "Larger!",
- type: "default",
- })
- .show();
- }
-
- @example
- static exampleShortcutSmallerCallback() {
- new ztoolkit.ProgressWindow(config.addonName)
- .createLine({
- text: "Smaller!",
- type: "default",
- })
- .show();
- }
-
- @example
- static exampleShortcutConflictingCallback() {
- const conflictingGroups = ztoolkit.Shortcut.checkAllKeyConflicting();
- new ztoolkit.ProgressWindow("Check Key Conflicting")
- .createLine({
- text: `${conflictingGroups.length} groups of conflicting keys found. Details are in the debug output/console.`,
- })
- .show(-1);
- ztoolkit.log(
- "Conflicting:",
- conflictingGroups,
- "All keys:",
- ztoolkit.Shortcut.getAll(),
- );
- }
-}
-
-export class UIExampleFactory {
- @example
- static registerStyleSheet() {
- const styles = ztoolkit.UI.createElement(document, "link", {
- properties: {
- type: "text/css",
- rel: "stylesheet",
- href: `chrome://${config.addonRef}/content/zoteroPane.css`,
- },
- });
- document.documentElement.appendChild(styles);
- document
- .getElementById("zotero-item-pane-content")
- ?.classList.add("makeItRed");
- }
-
- @example
- static registerRightClickMenuItem() {
- const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`;
- // item menuitem with icon
- ztoolkit.Menu.register("item", {
- tag: "menuitem",
- id: "zotero-itemmenu-addontemplate-test",
- label: getString("menuitem-label"),
- commandListener: (ev) => addon.hooks.onDialogEvents("dialogExample"),
- icon: menuIcon,
- });
- }
-
- @example
- static registerRightClickMenuPopup() {
- ztoolkit.Menu.register(
- "item",
- {
- tag: "menu",
- label: getString("menupopup-label"),
- children: [
- {
- tag: "menuitem",
- label: getString("menuitem-submenulabel"),
- oncommand: "alert('Hello World! Sub Menuitem.')",
- },
- ],
- },
- "before",
- document.querySelector(
- "#zotero-itemmenu-addontemplate-test",
- ) as XUL.MenuItem,
- );
- }
-
- @example
- static registerWindowMenuWithSeparator() {
- ztoolkit.Menu.register("menuFile", {
- tag: "menuseparator",
- });
- // menu->File menuitem
- ztoolkit.Menu.register("menuFile", {
- tag: "menuitem",
- label: getString("menuitem-filemenulabel"),
- oncommand: "alert('Hello World! File Menuitem.')",
- });
- }
-
- @example
- static async registerExtraColumn() {
- await ztoolkit.ItemTree.register(
- "test1",
- "text column",
- (
- field: string,
- unformatted: boolean,
- includeBaseMapped: boolean,
- item: Zotero.Item,
- ) => {
- return field + String(item.id);
- },
- {
- iconPath: "chrome://zotero/skin/cross.png",
- },
- );
- }
-
- @example
- static async registerExtraColumnWithCustomCell() {
- await ztoolkit.ItemTree.register(
- "test2",
- "custom column",
- (
- field: string,
- unformatted: boolean,
- includeBaseMapped: boolean,
- item: Zotero.Item,
- ) => {
- return String(item.id);
- },
- {
- renderCellHook(index, data, column) {
- const span = document.createElementNS(
- "http://www.w3.org/1999/xhtml",
- "span",
- );
- span.style.background = "#0dd068";
- span.innerText = "⭐" + data;
- return span;
- },
- },
- );
- }
-
- @example
- static async registerCustomCellRenderer() {
- await ztoolkit.ItemTree.addRenderCellHook(
- "title",
- // eslint-disable-next-line @typescript-eslint/ban-types
- (index: number, data: string, column: any, original: Function) => {
- const span = original(index, data, column) as HTMLSpanElement;
- span.style.background = "rgb(30, 30, 30)";
- span.style.color = "rgb(156, 220, 240)";
- return span;
- },
- );
- await ztoolkit.ItemTree.refresh();
- }
-
- @example
- static async registerCustomItemBoxRow() {
- await ztoolkit.ItemBox.register(
- "itemBoxFieldEditable",
- "Editable Custom Field",
- (field, unformatted, includeBaseMapped, item, original) => {
- return (
- ztoolkit.ExtraField.getExtraField(item, "itemBoxFieldEditable") || ""
- );
- },
- {
- editable: true,
- setFieldHook: (field, value, loadIn, item, original) => {
- window.alert("Custom itemBox value is changed and saved to extra!");
- ztoolkit.ExtraField.setExtraField(
- item,
- "itemBoxFieldEditable",
- value,
- );
- return true;
- },
- index: 1,
- },
- );
-
- await ztoolkit.ItemBox.register(
- "itemBoxFieldNonEditable",
- "Non-Editable Custom Field",
- (field, unformatted, includeBaseMapped, item, original) => {
- return (
- "[CANNOT EDIT THIS]" + (item.getField("title") as string).slice(0, 10)
- );
- },
- {
- editable: false,
- index: 2,
- },
- );
- }
-
- @example
- static registerLibraryTabPanel() {
- const tabId = ztoolkit.LibraryTabPanel.register(
- getString("tabpanel-lib-tab-label"),
- (panel: XUL.Element, win: Window) => {
- const elem = ztoolkit.UI.createElement(win.document, "vbox", {
- children: [
- {
- tag: "h2",
- properties: {
- innerText: "Hello World!",
- },
- },
- {
- tag: "div",
- properties: {
- innerText: "This is a library tab.",
- },
- },
- {
- tag: "button",
- namespace: "html",
- properties: {
- innerText: "Unregister",
- },
- listeners: [
- {
- type: "click",
- listener: () => {
- ztoolkit.LibraryTabPanel.unregister(tabId);
- },
- },
- ],
- },
- ],
- });
- panel.append(elem);
- },
- {
- targetIndex: 1,
- },
- );
- }
-
- @example
- static async registerReaderTabPanel() {
- const tabId = await ztoolkit.ReaderTabPanel.register(
- getString("tabpanel-reader-tab-label"),
- (
- panel: XUL.TabPanel | undefined,
- deck: XUL.Deck,
- win: Window,
- reader: _ZoteroTypes.ReaderInstance,
- ) => {
- if (!panel) {
- ztoolkit.log(
- "This reader do not have right-side bar. Adding reader tab skipped.",
- );
- return;
- }
- ztoolkit.log(reader);
- const elem = ztoolkit.UI.createElement(win.document, "vbox", {
- id: `${config.addonRef}-${reader._instanceID}-extra-reader-tab-div`,
- // This is important! Don't create content for multiple times
- // ignoreIfExists: true,
- removeIfExists: true,
- children: [
- {
- tag: "h2",
- properties: {
- innerText: "Hello World!",
- },
- },
- {
- tag: "div",
- properties: {
- innerText: "This is a reader tab.",
- },
- },
- {
- tag: "div",
- properties: {
- innerText: `Reader: ${reader._title.slice(0, 20)}`,
- },
- },
- {
- tag: "div",
- properties: {
- innerText: `itemID: ${reader.itemID}.`,
- },
- },
- {
- tag: "button",
- namespace: "html",
- properties: {
- innerText: "Unregister",
- },
- listeners: [
- {
- type: "click",
- listener: () => {
- ztoolkit.ReaderTabPanel.unregister(tabId);
- },
- },
- ],
- },
- ],
- });
- panel.append(elem);
- },
- {
- targetIndex: 1,
- },
- );
- }
-}
-
-export class PromptExampleFactory {
- @example
- static registerNormalCommandExample() {
- ztoolkit.Prompt.register([
- {
- name: "Normal Command Test",
- label: "Plugin Template",
- callback(prompt) {
- ztoolkit.getGlobal("alert")("Command triggered!");
- },
- },
- ]);
- }
-
- @example
- static registerAnonymousCommandExample() {
- ztoolkit.Prompt.register([
- {
- id: "search",
- callback: async (prompt) => {
- // https://github.com/zotero/zotero/blob/7262465109c21919b56a7ab214f7c7a8e1e63909/chrome/content/zotero/integration/quickFormat.js#L589
- function getItemDescription(item: Zotero.Item) {
- const nodes = [];
- let str = "";
- let author,
- authorDate = "";
- if (item.firstCreator) {
- author = authorDate = item.firstCreator;
- }
- let date = item.getField("date", true, true) as string;
- if (date && (date = date.substr(0, 4)) !== "0000") {
- authorDate += " (" + parseInt(date) + ")";
- }
- authorDate = authorDate.trim();
- if (authorDate) nodes.push(authorDate);
-
- const publicationTitle = item.getField(
- "publicationTitle",
- false,
- true,
- );
- if (publicationTitle) {
- nodes.push(`${publicationTitle}`);
- }
- let volumeIssue = item.getField("volume");
- const issue = item.getField("issue");
- if (issue) volumeIssue += "(" + issue + ")";
- if (volumeIssue) nodes.push(volumeIssue);
-
- const publisherPlace = [];
- let field;
- if ((field = item.getField("publisher")))
- publisherPlace.push(field);
- if ((field = item.getField("place"))) publisherPlace.push(field);
- if (publisherPlace.length) nodes.push(publisherPlace.join(": "));
-
- const pages = item.getField("pages");
- if (pages) nodes.push(pages);
-
- if (!nodes.length) {
- const url = item.getField("url");
- if (url) nodes.push(url);
- }
-
- // compile everything together
- for (let i = 0, n = nodes.length; i < n; i++) {
- const node = nodes[i];
-
- if (i != 0) str += ", ";
-
- if (typeof node === "object") {
- const label = document.createElement("label");
- label.setAttribute("value", str);
- label.setAttribute("crop", "end");
- str = "";
- } else {
- str += node;
- }
- }
- str.length && (str += ".");
- return str;
- }
- function filter(ids: number[]) {
- ids = ids.filter(async (id) => {
- const item = (await Zotero.Items.getAsync(id)) as Zotero.Item;
- return item.isRegularItem() && !(item as any).isFeedItem;
- });
- return ids;
- }
- const text = prompt.inputNode.value;
- prompt.showTip("Searching...");
- const s = new Zotero.Search();
- s.addCondition("quicksearch-titleCreatorYear", "contains", text);
- s.addCondition("itemType", "isNot", "attachment");
- let ids = await s.search();
- // prompt.exit will remove current container element.
- // @ts-ignore ignore
- prompt.exit();
- const container = prompt.createCommandsContainer();
- container.classList.add("suggestions");
- ids = filter(ids);
- console.log(ids.length);
- if (ids.length == 0) {
- const s = new Zotero.Search();
- const operators = [
- "is",
- "isNot",
- "true",
- "false",
- "isInTheLast",
- "isBefore",
- "isAfter",
- "contains",
- "doesNotContain",
- "beginsWith",
- ];
- let hasValidCondition = false;
- let joinMode = "all";
- if (/\s*\|\|\s*/.test(text)) {
- joinMode = "any";
- }
- text.split(/\s*(&&|\|\|)\s*/g).forEach((conditinString: string) => {
- const conditions = conditinString.split(/\s+/g);
- if (
- conditions.length == 3 &&
- operators.indexOf(conditions[1]) != -1
- ) {
- hasValidCondition = true;
- s.addCondition(
- "joinMode",
- joinMode as Zotero.Search.Operator,
- "",
- );
- s.addCondition(
- conditions[0] as string,
- conditions[1] as Zotero.Search.Operator,
- conditions[2] as string,
- );
- }
- });
- if (hasValidCondition) {
- ids = await s.search();
- }
- }
- ids = filter(ids);
- console.log(ids.length);
- if (ids.length > 0) {
- ids.forEach((id: number) => {
- const item = Zotero.Items.get(id);
- const title = item.getField("title");
- const ele = ztoolkit.UI.createElement(document, "div", {
- namespace: "html",
- classList: ["command"],
- listeners: [
- {
- type: "mousemove",
- listener: function () {
- // @ts-ignore ignore
- prompt.selectItem(this);
- },
- },
- {
- type: "click",
- listener: () => {
- prompt.promptNode.style.display = "none";
- Zotero_Tabs.select("zotero-pane");
- ZoteroPane.selectItem(item.id);
- },
- },
- ],
- styles: {
- display: "flex",
- flexDirection: "column",
- justifyContent: "start",
- },
- children: [
- {
- tag: "span",
- styles: {
- fontWeight: "bold",
- overflow: "hidden",
- textOverflow: "ellipsis",
- whiteSpace: "nowrap",
- },
- properties: {
- innerText: title,
- },
- },
- {
- tag: "span",
- styles: {
- overflow: "hidden",
- textOverflow: "ellipsis",
- whiteSpace: "nowrap",
- },
- properties: {
- innerHTML: getItemDescription(item),
- },
- },
- ],
- });
- container.appendChild(ele);
- });
- } else {
- // @ts-ignore ignore
- prompt.exit();
- prompt.showTip("Not Found.");
- }
- },
- },
- ]);
- }
-
- @example
- static registerConditionalCommandExample() {
- ztoolkit.Prompt.register([
- {
- name: "Conditional Command Test",
- label: "Plugin Template",
- // The when function is executed when Prompt UI is woken up by `Shift + P`, and this command does not display when false is returned.
- when: () => {
- const items = ZoteroPane.getSelectedItems();
- return items.length > 0;
- },
- callback(prompt) {
- prompt.inputNode.placeholder = "Hello World!";
- const items = ZoteroPane.getSelectedItems();
- ztoolkit.getGlobal("alert")(
- `You select ${items.length} items!\n\n${items
- .map(
- (item, index) =>
- String(index + 1) + ". " + item.getDisplayTitle(),
- )
- .join("\n")}`,
- );
- },
- },
- ]);
- }
-}
-
-export class HelperExampleFactory {
- @example
- static async dialogExample() {
- const dialogData: { [key: string | number]: any } = {
- inputValue: "test",
- checkboxValue: true,
- loadCallback: () => {
- ztoolkit.log(dialogData, "Dialog Opened!");
- },
- unloadCallback: () => {
- ztoolkit.log(dialogData, "Dialog closed!");
- },
- };
- const dialogHelper = new ztoolkit.Dialog(10, 2)
- .addCell(0, 0, {
- tag: "h1",
- properties: { innerHTML: "Helper Examples" },
- })
- .addCell(1, 0, {
- tag: "h2",
- properties: { innerHTML: "Dialog Data Binding" },
- })
- .addCell(2, 0, {
- tag: "p",
- properties: {
- innerHTML:
- "Elements with attribute 'data-bind' are binded to the prop under 'dialogData' with the same name.",
- },
- styles: {
- width: "200px",
- },
- })
- .addCell(3, 0, {
- tag: "label",
- namespace: "html",
- attributes: {
- for: "dialog-checkbox",
- },
- properties: { innerHTML: "bind:checkbox" },
- })
- .addCell(
- 3,
- 1,
- {
- tag: "input",
- namespace: "html",
- id: "dialog-checkbox",
- attributes: {
- "data-bind": "checkboxValue",
- "data-prop": "checked",
- type: "checkbox",
- },
- properties: { label: "Cell 1,0" },
- },
- false,
- )
- .addCell(4, 0, {
- tag: "label",
- namespace: "html",
- attributes: {
- for: "dialog-input",
- },
- properties: { innerHTML: "bind:input" },
- })
- .addCell(
- 4,
- 1,
- {
- tag: "input",
- namespace: "html",
- id: "dialog-input",
- attributes: {
- "data-bind": "inputValue",
- "data-prop": "value",
- type: "text",
- },
- },
- false,
- )
- .addCell(5, 0, {
- tag: "h2",
- properties: { innerHTML: "Toolkit Helper Examples" },
- })
- .addCell(
- 6,
- 0,
- {
- tag: "button",
- namespace: "html",
- attributes: {
- type: "button",
- },
- listeners: [
- {
- type: "click",
- listener: (e: Event) => {
- addon.hooks.onDialogEvents("clipboardExample");
- },
- },
- ],
- children: [
- {
- tag: "div",
- styles: {
- padding: "2.5px 15px",
- },
- properties: {
- innerHTML: "example:clipboard",
- },
- },
- ],
- },
- false,
- )
- .addCell(
- 7,
- 0,
- {
- tag: "button",
- namespace: "html",
- attributes: {
- type: "button",
- },
- listeners: [
- {
- type: "click",
- listener: (e: Event) => {
- addon.hooks.onDialogEvents("filePickerExample");
- },
- },
- ],
- children: [
- {
- tag: "div",
- styles: {
- padding: "2.5px 15px",
- },
- properties: {
- innerHTML: "example:filepicker",
- },
- },
- ],
- },
- false,
- )
- .addCell(
- 8,
- 0,
- {
- tag: "button",
- namespace: "html",
- attributes: {
- type: "button",
- },
- listeners: [
- {
- type: "click",
- listener: (e: Event) => {
- addon.hooks.onDialogEvents("progressWindowExample");
- },
- },
- ],
- children: [
- {
- tag: "div",
- styles: {
- padding: "2.5px 15px",
- },
- properties: {
- innerHTML: "example:progressWindow",
- },
- },
- ],
- },
- false,
- )
- .addCell(
- 9,
- 0,
- {
- tag: "button",
- namespace: "html",
- attributes: {
- type: "button",
- },
- listeners: [
- {
- type: "click",
- listener: (e: Event) => {
- addon.hooks.onDialogEvents("vtableExample");
- },
- },
- ],
- children: [
- {
- tag: "div",
- styles: {
- padding: "2.5px 15px",
- },
- properties: {
- innerHTML: "example:virtualized-table",
- },
- },
- ],
- },
- false,
- )
- .addButton("Confirm", "confirm")
- .addButton("Cancel", "cancel")
- .addButton("Help", "help", {
- noClose: true,
- callback: (e) => {
- dialogHelper.window?.alert(
- "Help Clicked! Dialog will not be closed.",
- );
- },
- })
- .setDialogData(dialogData)
- .open("Dialog Example");
- addon.data.dialog = dialogHelper;
- await dialogData.unloadLock.promise;
- addon.data.dialog = undefined;
- addon.data.alive &&
- ztoolkit.getGlobal("alert")(
- `Close dialog with ${dialogData._lastButtonId}.\nCheckbox: ${dialogData.checkboxValue}\nInput: ${dialogData.inputValue}.`,
- );
- ztoolkit.log(dialogData);
- }
-
- @example
- static clipboardExample() {
- new ztoolkit.Clipboard()
- .addText(
- "",
- "text/unicode",
- )
- .addText(
- 'Plugin Template',
- "text/html",
- )
- .copy();
- ztoolkit.getGlobal("alert")("Copied!");
- }
-
- @example
- static async filePickerExample() {
- const path = await new ztoolkit.FilePicker(
- "Import File",
- "open",
- [
- ["PNG File(*.png)", "*.png"],
- ["Any", "*.*"],
- ],
- "image.png",
- ).open();
- ztoolkit.getGlobal("alert")(`Selected ${path}`);
- }
-
- @example
- static progressWindowExample() {
- new ztoolkit.ProgressWindow(config.addonName)
- .createLine({
- text: "ProgressWindow Example!",
- type: "success",
- progress: 100,
- })
- .show();
- }
-
- @example
- static vtableExample() {
- ztoolkit.getGlobal("alert")("See src/modules/preferenceScript.ts");
- }
-}
diff --git a/src/modules/preferenceScript.ts b/src/modules/preferenceScript.ts
index ef6e5b9..790be70 100644
--- a/src/modules/preferenceScript.ts
+++ b/src/modules/preferenceScript.ts
@@ -7,125 +7,12 @@ export async function registerPrefsScripts(_window: Window) {
if (!addon.data.prefs) {
addon.data.prefs = {
window: _window,
- columns: [
- {
- dataKey: "title",
- label: getString("prefs-table-title"),
- fixedWidth: true,
- width: 100,
- },
- {
- dataKey: "detail",
- label: getString("prefs-table-detail"),
- },
- ],
- rows: [
- {
- title: "Orange",
- detail: "It's juicy",
- },
- {
- title: "Banana",
- detail: "It's sweet",
- },
- {
- title: "Apple",
- detail: "I mean the fruit APPLE",
- },
- ],
};
} else {
addon.data.prefs.window = _window;
}
- updatePrefsUI();
bindPrefEvents();
}
-async function updatePrefsUI() {
- // You can initialize some UI elements on prefs window
- // with addon.data.prefs.window.document
- // Or bind some events to the elements
- const renderLock = ztoolkit.getGlobal("Zotero").Promise.defer();
- if (addon.data.prefs?.window == undefined) return;
- const tableHelper = new ztoolkit.VirtualizedTable(addon.data.prefs?.window)
- .setContainerId(`${config.addonRef}-table-container`)
- .setProp({
- id: `${config.addonRef}-prefs-table`,
- // Do not use setLocale, as it modifies the Zotero.Intl.strings
- // Set locales directly to columns
- columns: addon.data.prefs?.columns,
- showHeader: true,
- multiSelect: true,
- staticColumns: true,
- disableFontSizeScaling: true,
- })
- .setProp("getRowCount", () => addon.data.prefs?.rows.length || 0)
- .setProp(
- "getRowData",
- (index) =>
- addon.data.prefs?.rows[index] || {
- title: "no data",
- detail: "no data",
- },
- )
- // Show a progress window when selection changes
- .setProp("onSelectionChange", (selection) => {
- new ztoolkit.ProgressWindow(config.addonName)
- .createLine({
- text: `Selected line: ${addon.data.prefs?.rows
- .filter((v, i) => selection.isSelected(i))
- .map((row) => row.title)
- .join(",")}`,
- progress: 100,
- })
- .show();
- })
- // When pressing delete, delete selected line and refresh table.
- // Returning false to prevent default event.
- .setProp("onKeyDown", (event: KeyboardEvent) => {
- if (event.key == "Delete" || (Zotero.isMac && event.key == "Backspace")) {
- addon.data.prefs!.rows =
- addon.data.prefs?.rows.filter(
- (v, i) => !tableHelper.treeInstance.selection.isSelected(i),
- ) || [];
- tableHelper.render();
- return false;
- }
- return true;
- })
- // For find-as-you-type
- .setProp(
- "getRowString",
- (index) => addon.data.prefs?.rows[index].title || "",
- )
- // Render the table.
- .render(-1, () => {
- renderLock.resolve();
- });
- await renderLock.promise;
- ztoolkit.log("Preference table rendered!");
-}
-
function bindPrefEvents() {
- addon.data
- .prefs!.window.document.querySelector(
- `#zotero-prefpane-${config.addonRef}-enable`,
- )
- ?.addEventListener("command", (e) => {
- ztoolkit.log(e);
- addon.data.prefs!.window.alert(
- `Successfully changed to ${(e.target as XUL.Checkbox).checked}!`,
- );
- });
-
- addon.data
- .prefs!.window.document.querySelector(
- `#zotero-prefpane-${config.addonRef}-input`,
- )
- ?.addEventListener("change", (e) => {
- ztoolkit.log(e);
- addon.data.prefs!.window.alert(
- `Successfully changed to ${(e.target as HTMLInputElement).value}!`,
- );
- });
}
diff --git a/src/modules/tldrFetcher.ts b/src/modules/tldrFetcher.ts
new file mode 100644
index 0000000..f523005
--- /dev/null
+++ b/src/modules/tldrFetcher.ts
@@ -0,0 +1,157 @@
+import { DataStorage } from "./dataStorage";
+
+type SemanticScholarItemInfo = {
+ title?: string,
+ abstract?: string,
+ tldr?: string,
+};
+
+export const TLDRFieldKey = 'TLDR';
+export const TLDRUnrelated = 'tldr-unrelated' // semantic scholar 找到了该item,但是该item没有tldr
+export const TLDRItemNotFound = 'tldr-itemnotfound' // semantic scholar 找不到该item,
+
+export class TLDRFetcher {
+ private readonly zoteroItem: Zotero.Item;
+ private readonly title?: string;
+ private readonly abstract?: string;
+
+ constructor(item: Zotero.Item) {
+ this.zoteroItem = item;
+ if (item.isRegularItem() && !item.isCollection()) {
+ this.title = item.getField('title') as string;
+ this.abstract = item.getField('abstractNote') as string;
+ }
+ }
+
+ async fetchTLDR() {
+ if (!this.title || this.title.length <= 0) { return false; }
+ try {
+ const infos = await this.fetchRelevanceItemInfos(this.title);
+ for (const info of infos) {
+ let match = false;
+ if (info.title && this.title && this.checkLCS(info.title, this.title)) {
+ match = true;
+ } else if (info.abstract && this.abstract && this.checkLCS(info.abstract, this.abstract)) {
+ match = true;
+ }
+ if (match) {
+ const result = info.tldr ?? TLDRUnrelated;
+ DataStorage.instance(TLDRFieldKey).modify((data: any) => {
+ data[this.zoteroItem.id] = result;
+ return data;
+ });
+ return true;
+ }
+ }
+ DataStorage.instance(TLDRFieldKey).modify((data: any) => {
+ data[this.zoteroItem.id] = TLDRItemNotFound;
+ return data;
+ });
+ return false;
+ } catch (error) {
+ Zotero.log(`post semantic scholar request error: ${error}`);
+ return false;
+ }
+ }
+
+ private async fetchRelevanceItemInfos(title: string): Promise {
+ const semanticScholarURL = 'https://www.semanticscholar.org/api/1/search';
+ const params = {
+ queryString: title,
+ page: 1,
+ pageSize: 10,
+ sort: 'relevance',
+ authors: [],
+ coAuthors: [],
+ venues: [],
+ performTitleMatch: true,
+ requireViewablePdf: false,
+ includeTldrs: true,
+ };
+ const resp = await Zotero.HTTP.request("POST", semanticScholarURL, {
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify(params),
+ });
+ if (resp.status === 200) {
+ const results = JSON.parse(resp.response).results;
+ return results.map((item: any) => {
+ const result = {
+ title: item.title.text,
+ abstract: item.paperAbstract.text,
+ tldr: undefined,
+ };
+ if (item.tldr) {
+ result.tldr = item.tldr.text;
+ }
+ return result;
+ });
+ }
+ return [];
+ }
+
+ private checkLCS(pattern: string, content: string): boolean {
+ const LCS = StringMatchUtils.longestCommonSubsequence(pattern, content);
+ return LCS.length >= Math.max(pattern.length, content.length) * 0.9
+ }
+}
+
+
+class StringMatchUtils {
+ static longestCommonSubsequence(text1: string, text2: string): string {
+ const m = text1.length;
+ const n = text2.length;
+
+ const dp: number[][] = new Array(m + 1);
+ for (let i = 0; i <= m; i++) {
+ dp[i] = new Array(n + 1).fill(0);
+ }
+
+ for (let i = 1; i <= m; i++) {
+ for (let j = 1; j <= n; j++) {
+ if (text1[i - 1] === text2[j - 1]) {
+ dp[i][j] = dp[i - 1][j - 1] + 1;
+ } else {
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
+ }
+ }
+ }
+
+ let i = m, j = n;
+ const lcs: string[] = [];
+ while (i > 0 && j > 0) {
+ if (text1[i - 1] === text2[j - 1]) {
+ lcs.unshift(text1[i - 1]);
+ i--;
+ j--;
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
+ i--;
+ } else {
+ j--;
+ }
+ }
+
+ return lcs.join('');
+ }
+
+ // static minWindow(s: string, t: string): [number, number] | null {
+ // const m = s.length, n = t.length
+ // let start = -1, minLen = Number.MAX_SAFE_INTEGER, i = 0, j = 0, end;
+ // while (i < m) {
+ // if (s[i] == t[j]) {
+ // if (++j == n) {
+ // end = i + 1;
+ // while (--j >= 0) {
+ // while (s[i--] != t[j]);
+ // }
+ // ++i; ++j;
+ // if (end - i < minLen) {
+ // minLen = end - i;
+ // start = i;
+ // }
+ // }
+ // }
+ // ++i;
+ // }
+ // return start == -1 ? null : [start, minLen];
+ // }
+}
\ No newline at end of file