Showing preview only (397K chars total). Download the full file or copy to clipboard to get everything.
Repository: l0o0/jasminum
Branch: main
Commit: e601b35dc67c
Files: 88
Total size: 373.6 KB
Directory structure:
gitextract_udbmtm46/
├── .gitattributes
├── .github/
│ ├── dependabot.yml
│ ├── renovate.json
│ └── workflows/
│ └── release.yml
├── .gitignore
├── .prettierignore
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── toolkit.code-snippets
├── LICENSE
├── README.md
├── addon/
│ ├── bootstrap.js
│ ├── chrome/
│ │ └── content/
│ │ ├── preferences-main.xhtml
│ │ ├── preferences-translators.xhtml
│ │ ├── prefpanel.css
│ │ └── progress.xhtml
│ ├── locale/
│ │ ├── en-US/
│ │ │ ├── addon.ftl
│ │ │ ├── mainWindow.ftl
│ │ │ ├── preferences-main.ftl
│ │ │ ├── preferences-translators.ftl
│ │ │ └── progress.ftl
│ │ ├── zh-CN/
│ │ │ ├── addon.ftl
│ │ │ ├── mainWindow.ftl
│ │ │ ├── preferences-main.ftl
│ │ │ ├── preferences-translators.ftl
│ │ │ └── progress.ftl
│ │ └── zh-TW/
│ │ ├── addon.ftl
│ │ ├── mainWindow.ftl
│ │ ├── preferences-main.ftl
│ │ ├── preferences-translators.ftl
│ │ └── progress.ftl
│ ├── manifest.json
│ └── prefs.js
├── doc/
│ └── README-zhCN.md
├── eslint.config.mjs
├── package.json
├── src/
│ ├── addon.ts
│ ├── hooks.ts
│ ├── index.ts
│ ├── modules/
│ │ ├── attachments/
│ │ │ ├── index.ts
│ │ │ └── localMatch.ts
│ │ ├── menu.ts
│ │ ├── notifier.ts
│ │ ├── outline/
│ │ │ ├── bookmark.ts
│ │ │ ├── events.ts
│ │ │ ├── index.ts
│ │ │ ├── outline.ts
│ │ │ └── style.ts
│ │ ├── preferences/
│ │ │ ├── main.ts
│ │ │ └── translators.ts
│ │ ├── progress.ts
│ │ ├── services/
│ │ │ ├── cnki.ts
│ │ │ ├── index.ts
│ │ │ ├── pubscholar.ts
│ │ │ └── yiigle.ts
│ │ ├── styles.ts
│ │ ├── tools.ts
│ │ ├── translators.ts
│ │ ├── workers/
│ │ │ ├── index.ts
│ │ │ └── outline.ts
│ │ └── wps.ts
│ └── utils/
│ ├── cookiebox.ts
│ ├── detect.ts
│ ├── http.ts
│ ├── locale.ts
│ ├── pattern.ts
│ ├── pdfParser.ts
│ ├── prefs.ts
│ ├── task.ts
│ ├── wait.ts
│ ├── window.ts
│ └── ztoolkit.ts
├── test/
│ ├── CNKI_translator_test.js
│ ├── expert_china.json
│ └── expert_oversea.json
├── tsconfig.json
├── typings/
│ ├── attachment.d.ts
│ ├── global.d.ts
│ ├── i10n.d.ts
│ ├── myzotero.d.ts
│ ├── notifier.d.ts
│ ├── outline.d.ts
│ ├── pdfParser.d.ts
│ ├── prefs.d.ts
│ ├── scrape.d.ts
│ └── translators.d.ts
└── zotero-plugin.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
================================================
FILE: .github/renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticPrefixChore",
":prHourlyLimitNone",
":prConcurrentLimitNone",
":enableVulnerabilityAlerts",
":dependencyDashboard",
"group:allNonMajor",
"schedule:weekly"
],
"labels": ["dependencies"],
"packageRules": [
{
"matchPackageNames": [
"zotero-plugin-toolkit",
"zotero-types",
"zotero-plugin-scaffold"
],
"schedule": ["at any time"],
"automerge": true
}
],
"git-submodules": {
"enabled": true
}
}
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- v**
workflow_dispatch: # 新增手动触发入口
inputs:
version:
description: 'Release version (格式 vX.Y.Z)'
required: true
skip-notification:
description: '跳过通知 (true/false)'
default: 'false'
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# 手动触发时创建标签
- name: Create tag (manual)
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git tag ${{ inputs.version }}
git push origin ${{ inputs.version }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22.14
- name: Install deps
run: npm install -f
- name: Build
run: npm run build
- name: Release to GitHub
run: npm run release
- name: Notify release
if: ${{ !contains(github.event.inputs.skip-notification, 'true') }}
uses: apexskier/github-release-commenter@v1
continue-on-error: true
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
comment-template: |
:rocket: _This ticket has been resolved in {release_tag}. See {release_link} for release notes._
================================================
FILE: .gitignore
================================================
build
logs
node_modules
pnpm-lock.yaml
yarn.lock
.DS_Store
.env
.scaffold
tmp/
================================================
FILE: .prettierignore
================================================
.vscode
build
logs
node_modules
package-lock.json
yarn.lock
pnpm-lock.yaml
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"macabeus.vscode-fluent"
]
}
================================================
FILE: .vscode/launch.json
================================================
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Start",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start"]
},
{
"type": "node",
"request": "launch",
"name": "Build",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "build"]
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"[javascript]": {
"editor.defaultIndentSize": 2,
"editor.tabSize": 2
},
"[typescript]": {
"editor.defaultIndentSize": 2,
"editor.tabSize": 2
},
"files.eol": "\n",
"editor.formatOnType": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["javascript", "typescript"],
"prettier.requireConfig": true,
"commentTranslate.hover.enabled": true,
"editor.detectIndentation": false, // 关键:禁止自动检测
"prettier.tabWidth": 2,
"eslint.options": {
"overrideConfig": {
"rules": {
"indent": "off" // 强制禁用 ESLint 缩进规则
}
}
}
}
================================================
FILE: .vscode/toolkit.code-snippets
================================================
{
"appendElement - full": {
"scope": "javascript,typescript",
"prefix": "appendElement",
"body": [
"appendElement({",
"\ttag: '${1:div}',",
"\tid: '${2:id}',",
"\tnamespace: '${3:html}',",
"\tclassList: ['${4:class}'],",
"\tstyles: {${5:style}: '$6'},",
"\tproperties: {},",
"\tattributes: {},",
"\t[{ '${7:onload}', (e: Event) => $8, ${9:false} }],",
"\tcheckExistanceParent: ${10:HTMLElement},",
"\tignoreIfExists: ${11:true},",
"\tskipIfExists: ${12:true},",
"\tremoveIfExists: ${13:true},",
"\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},",
"\tchildren: [$15]",
"}, ${16:container});"
]
},
"appendElement - minimum": {
"scope": "javascript,typescript",
"prefix": "appendElement",
"body": "appendElement({ tag: '$1' }, $2);"
},
"register Notifier": {
"scope": "javascript,typescript",
"prefix": "registerObserver",
"body": [
"registerObserver({",
"\t notify: (",
"\t\tevent: _ZoteroTypes.Notifier.Event,",
"\t\ttype: _ZoteroTypes.Notifier.Type,",
"\t\tids: string[],",
"\t\textraData: _ZoteroTypes.anyObj",
"\t) => {",
"\t\t$0",
"\t}",
"});"
]
}
}
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
================================================
FILE: README.md
================================================
<div align=center>

# 茉莉花 Jasminum
[](https://www.zotero.org) [](https://github.com/windingwind/zotero-plugin-template) 
</div>
</br>
简体中文 | [English](doc/README-en.md)
## 1. 基础功能
- 中文PDF元数据抓取
- 中文转换器下载,转换器来源于 Zotero中文社区 [translators_CN](https://github.com/l0o0/translators_CN)
- 中文引用格式下载,引用格式来源于项目 Zotero中文社区 [styles](https://github.com/zotero-chinese/styles)
- 小工具
- 语言设置
- 中文姓名拆分与合并
## 2.使用教程
### 2.1 元数据抓取
目前支持仅支持从**中国知网**获取元数据,后续考虑会添加其他数据来源。
在 Zotero 中添加中文附件后,右键附件,在菜单栏选择`茉莉花抓取` -> `抓取期刊元数据`,在弹出窗口可以看到元数据抓取的结果。
如果有多个搜索结果,需要你手动选择最匹配的结果,再点击确认,即可完成抓取。

### 2.3 本地附件匹配功能
在使用 Zotero Connector 在浏览器上抓取中文期刊时(尤其是中国知网),经常出现元数据抓取成功而附件无法下载自动的异常,当你手动下载期刊附件(PDF/CAJ)后,可以方便地用此功能来将下载的附件与元数据匹配。
右键期刊条目,`小工具` -> `在下载文件夹中查找附件`,该功能会自动在当前`下载目录`中寻找与当前条目匹配的附件,匹配规划是**根据期刊标题与文件名的匹配度**。
`下载目录`默认是系统的下载目录,Windows系统默认是`C:\Users\用户名\Downloads`,Mac系统默认是`/Users/用户名/Downloads`,Linux系统默认是`/home/用户名/Downloads`。也可以在`设置`中修改下载目录。
下载目录中匹配成功的附件默认会移动到备份目录中`下载目录/jasminum-backup`中,在设置中还可以选择
- 删除匹配成功的附件。匹配到元数据的附件已经保存到Zotero中,可以放心删除下载目录中的附件(个人建议删除,避免下载目录中附件过多)。
- 无须处理。即使匹配成功,附件还是会在下载目录中,当然Zotero已经保存了一份。
### 2.3 PDF大纲
在 PDF 阅读窗口的左侧边栏中,点击茉莉花书签按钮,即可看到书签大纲窗口。

最上方的5个按钮,功能分别是:
- 展开所有书签
- 折叠所有书签
- 添加书签
- 删除书签
- 将书签内容保存到PDF(默认只以配置文件的形式保存到本地)
**键盘快捷键导航**
- 键盘↑,上一个书签(跳过折叠内容)
- 键盘↓,下一个书签(跳过折叠内容)
- 键盘←或→,展开或折叠节点
- 空格键,编辑书签内容
- [,将书签移到上一级(作为原上级节点的下一个相邻节点)
- ],将书签移到下一级(自动将相邻的上一个节点作为上级节点)
- \,创建新节点(默认作为选中节点的子节点)
- Delete 或 Backspace,删除节点
## 3. ❤️致谢
特别感谢 [jiaojiaodubai](https://github.com/jiaojiaodubai) 同学,长期以来对 [translators_CN](https://github.com/l0o0/translators_CN) 和 本项目 的贡献。
================================================
FILE: addon/bootstrap.js
================================================
/* eslint-disable no-undef */
/**
* Most of this code is from Zotero team's official Make It Red example[1]
* or the Zotero 7 documentation[2].
* [1] https://github.com/zotero/make-it-red
* [2] https://www.zotero.org/support/dev/zotero_7_for_developers
*/
var chromeHandle;
function install(data, reason) {}
async function startup({ id, version, resourceURI, rootURI }, reason) {
await Zotero.initializationPromise;
// String 'rootURI' introduced in Zotero 7
if (!rootURI) {
rootURI = resourceURI.spec;
}
var aomStartup = Components.classes[
"@mozilla.org/addons/addon-manager-startup;1"
].getService(Components.interfaces.amIAddonManagerStartup);
var manifestURI = Services.io.newURI(rootURI + "manifest.json");
chromeHandle = aomStartup.registerChrome(manifestURI, [
["content", "__addonRef__", rootURI + "chrome/content/"],
]);
/**
* Global variables for plugin code.
* The `_globalThis` is the global root variable of the plugin sandbox environment
* and all child variables assigned to it is globally accessible.
* See `src/index.ts` for details.
*/
const ctx = {
rootURI,
};
ctx._globalThis = ctx;
Services.scriptloader.loadSubScript(
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
ctx,
);
Zotero.__addonInstance__.hooks.onStartup();
}
async function onMainWindowLoad({ window }, reason) {
Zotero.__addonInstance__?.hooks.onMainWindowLoad(window);
}
async function onMainWindowUnload({ window }, reason) {
Zotero.__addonInstance__?.hooks.onMainWindowUnload(window);
}
function shutdown({ id, version, resourceURI, rootURI }, reason) {
if (reason === APP_SHUTDOWN) {
return;
}
if (typeof Zotero === "undefined") {
Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
Components.interfaces.nsISupports,
).wrappedJSObject;
}
Zotero.__addonInstance__?.hooks.onShutdown();
Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Components.interfaces.nsIStringBundleService)
.flushBundles();
Cu.unload(`${rootURI}/chrome/content/scripts/__addonRef__.js`);
if (chromeHandle) {
chromeHandle.destruct();
chromeHandle = null;
}
}
function uninstall(data, reason) {}
================================================
FILE: addon/chrome/content/preferences-main.xhtml
================================================
<linkset>
<html:link rel="localization" href="__addonRef__-preferences-main.ftl" />
<html:link
xmlns:html="http://www.w3.org/1999/xhtml"
rel="stylesheet"
href="chrome://__addonRef__/content/prefpanel.css"
/>
</linkset>
<!-- 元数据抓取 -->
<groupbox onload="Zotero.__addonInstance__.hooks.onPrefsWindowLoad(window);">
<label><html:h2 data-l10n-id="pref-group-metadata"></html:h2></label>
<checkbox
id="zotero-prefpane-__addonRef__-isMainlandChina"
native="true"
preference="isMainlandChina"
data-l10n-id="label-isMainlandChina"
/>
<checkbox
id="zotero-prefpane-__addonRef__-autoupdate"
native="true"
preference="autoUpdateMetadata"
data-l10n-id="label-autoupdate-metadata"
/>
<hbox flex="2" class="inputbox">
<menulist
id="zotero-prefpane-__addonRef__-namepattern-menulist"
native="true"
>
<menupopup>
<menuitem value="auto" data-l10n-id="label-namepattern-auto" />
<menuitem value="{%t}_{%g}" data-l10n-id="label-namepattern-tg" />
<menuitem value="{%t}" data-l10n-id="label-namepattern-t" />
<menuseparator></menuseparator>
<menuitem value="custom" data-l10n-id="label-namepattern-custom" />
</menupopup>
</menulist>
<html:input
type="text"
class="auto_width"
id="zotero-prefpane-__addonRef__-namepattern-input"
preference="namePattern"
>
</html:input>
<html:input
type="text"
class="auto_width hidden"
id="zotero-prefpane-__addonRef__-namepatternCustom-input"
preference="namePatternCustom"
>
</html:input>
<image
class="help-icon"
src="chrome://jasminum/content/icons/help.svg"
data-l10n-id="namepattern-desc"
></image>
</hbox>
<!-- <hbox align="center">
<label
for="zotero-prefpane-__addonRef__-metadata-source"
data-l10n-id="label-metadata-source"
></label>
<html:input
type="text"
id="zotero-prefpane-__addonRef__-metadata-source-input"
preference="metadataSource"
disabled="true"
></html:input>
<html:div>
<button
id="zotero-prefpane-__addonRef__-metadata-source-button"
data-l10n-id="label-choose-source"
></button>
<html:div id="metadata-source-dropdown" class="dropdown-content">
<checkbox
disabled="true"
native="true"
class="metadata-drop-item"
value="CNKI"
data-l10n-id="label-metadata-source-cnki"
/>
<checkbox
class="metadata-drop-item"
native="true"
value="CVIP"
data-l10n-id="label-metadata-source-cvip"
/>
</html:div>
</html:div>
</hbox> -->
</groupbox>
<!-- 转换器 -->
<groupbox>
<label><html:h2 data-l10n-id="pref-group-translators"></html:h2></label>
<hbox align="center">
<checkbox
id="zotero-prefpane-__addonRef__-autoUpdateTranslators"
native="true"
preference="autoUpdateTranslators"
data-l10n-id="label-auto-update-translators"
/>
<button
id="zotero-prefpane-__addonRef__-force-update"
data-l10n-id="label-translators-force-update"
style="margin-left: 4px"
></button>
</hbox>
<hbox align="center">
<label data-l10n-id="label-translators-detail">click</label>
<button
id="zotero-prefpane-__addonRef__-open-translator-table"
data-l10n-id="label-translators-detail-click"
></button>
</hbox>
<hbox align="center">
<label data-l10n-id="label-translator-source">click</label>
<menulist
id="zotero-prefpane-__addonRef__-source"
preference="translatorSource"
>
<menupopup>
<menuitem
value="https://ftp.zotero-chinese.com/translators_CN"
label="Zotero中文"
/>
<menuitem
value="https://oss.wwang.de/translators_CN"
label="可口可乐"
/>
<menuitem
value="https://www.wieke.cn/translators_CN"
label="www.wieke.cn"
/>
</menupopup>
</menulist>
<button
id="zotero-prefpane-__addonRef__-best-speed-button"
data-l10n-id="label-best-speed"
></button>
<image
class="help-icon"
src="chrome://jasminum/content/icons/help.svg"
data-l10n-id="translatorSource-desc"
></image>
</hbox>
</groupbox>
<!-- 附件 -->
<groupbox>
<label><html:h2 data-l10n-id="pref-group-attachment"></html:h2></label>
<hbox align="center">
<label
for="zotero-prefpane-__addonRef__-pdf-match-folder"
data-l10n-id="label-pdf-match-folder"
></label>
<html:input
type="text"
id="zotero-prefpane-__addonRef__-pdf-match-folder-input"
preference="pdfMatchFolder"
></html:input>
<button
id="zotero-prefpane-__addonRef__-pdf-match-folder-button"
data-l10n-id="label-choose-folder"
></button>
<image
class="help-icon"
src="chrome://jasminum/content/icons/help.svg"
data-l10n-id="attachment-folder-desc"
></image>
</hbox>
<hbox align="center">
<label data-l10n-id="action-after-import"></label>
<menulist
id="zotero-prefpane-__addonRef__-source"
preference="actionAfterAttachmentImport"
>
<menupopup>
<menuitem value="nothing" data-l10n-id="nothing-label" />
<menuitem value="backup" data-l10n-id="backup-label" />
<menuitem value="delete" data-l10n-id="delete-label" />
</menupopup>
</menulist>
<image
class="help-icon"
src="chrome://jasminum/content/icons/help.svg"
data-l10n-id="action-after-import-desc"
></image>
</hbox>
</groupbox>
<!-- 书签 -->
<groupbox>
<label><html:h2 data-l10n-id="pref-group-bookmark"></html:h2></label>
<hbox align="center">
<checkbox
id="zotero-prefpane-__addonRef__-enableBookmark"
native="true"
preference="enableBookmark"
data-l10n-id="label-enableBookmark"
/>
<image
class="help-icon"
src="chrome://jasminum/content/icons/help.svg"
data-l10n-id="outline-desc"
></image>
</hbox>
<checkbox
id="zotero-prefpane-__addonRef__-disableZoteroOutline"
native="true"
preference="disableZoteroOutline"
data-l10n-id="label-disableZoteroOutline"
/>
</groupbox>
<!-- 小工具 -->
<groupbox>
<label><html:h2 data-l10n-id="pref-group-tools"></html:h2></label>
<checkbox
id="zotero-prefpane-__addonRef__-autoSplitName"
native="true"
preference="autoSplitName"
data-l10n-id="label-auto-split-name"
/>
<checkbox
id="zotero-prefpane-__addonRef__-splitEnName"
native="true"
preference="splitEnName"
data-l10n-id="label-split-en-name"
/>
<hbox align="center">
<label
for="zotero-prefpane-__addonRef__-language"
data-l10n-id="label-language"
></label>
<html:input
type="text"
id="zotero-prefpane-__addonRef__-language"
preference="language"
></html:input>
</hbox>
<hbox>
<label
data-l10n-id="label-tools-info-1"
style="margin-top: 8px; margin-bottom: 8px"
></label>
<label
data-l10n-id="label-tools-linter"
is="zotero-text-link"
class="zotero-text-link"
href="https://zotero-chinese.com/user-guide/plugins/linter"
style="margin-top: 8px; margin-bottom: 8px"
></label>
<label
data-l10n-id="label-tools-info-2"
style="margin-left: 0.5ch; margin-top: 8px; margin-bottom: 8px"
></label>
</hbox>
</groupbox>
<!-- WPS下载 -->
<!-- <groupbox>
<label><html:h2 data-l10n-id="pref-group-wps"></html:h2></label>
<hbox align="center">
<label data-l10n-id="label-wps"></label>
<button
id="zotero-prefpane-__addonRef__-install-wps-plugin-button"
data-l10n-id="label-install-wps-plugin-click"
></button>
<label
data-l10n-id="label-wps-help"
is="zotero-text-link"
class="zotero-text-link"
href="https://zotero-chinese.com/"
></label>
</hbox>
</groupbox> -->
<!-- 介绍及致谢 -->
<groupbox>
<label><html:h2 data-l10n-id="pref-group-about"></html:h2></label>
<hbox>
<label
value="Jasminum "
class="zotero-text-link"
is="zotero-text-link"
href="https://github.com/l0o0/jasminum"
style="margin-right: 4px"
></label>
<label
data-l10n-id="pref-help"
data-l10n-args='{"time": "__buildTime__","name": "__addonName__","version":"__buildVersion__"}'
></label>
<label
data-l10n-id="label-zotero-chinese"
class="zotero-text-link"
is="zotero-text-link"
href="https://zotero-chinese.com/"
></label>
</hbox>
</groupbox>
================================================
FILE: addon/chrome/content/preferences-translators.xhtml
================================================
<?xml version="1.0"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css" type="text/css"?>
<!-- prettier-ignore -->
<!DOCTYPE html>
<html
id="__addonRef__-translators"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="__addonRef__:translators"
>
<head>
<title data-l10n-id="title"></title>
<meta charset="utf-8" />
<xul:linkset>
<link rel="localization" href="zotero.ftl" />
<link
rel="localization"
href="__addonRef__-preferences-translators.ftl"
/>
</xul:linkset>
<xul:keyset>
<xul:key
id="key_close"
key="W"
modifiers="accel"
oncommand="window.close()"
/>
</xul:keyset>
<script>
document.addEventListener("DOMContentLoaded", (ev) => {
Services.scriptloader.loadSubScript(
"chrome://zotero/content/include.js",
this,
);
window.arguments[0]._initPromise.resolve();
});
// Custom elements
Services.scriptloader.loadSubScript(
"chrome://zotero/content/customElements.js",
this,
);
</script>
<style>
html,
body {
margin: 0;
min-width: 800px;
min-height: 400px;
height: 100%;
}
header,
footer {
display: flex;
align-items: center;
height: 50px;
padding: 0 10px;
}
toolbar,
toolbar > hbox {
width: 100%;
}
.toolbarbutton-text {
margin-left: 4px;
}
main {
width: 100%;
height: calc(100% - 2 * 50px);
background: var(--material-background);
}
#table-container {
height: 100%;
width: 100%;
}
#translators-table {
height: 100%;
}
footer {
justify-content: space-between;
}
footer > * {
display: flex;
gap: 6px;
}
#links > .zotero-text-link {
margin: 0 5px;
}
footer > * > :first-child {
margin-left: 0;
}
footer > * > :last-child {
margin-right: 0;
}
</style>
</head>
<body>
<header
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
>
<toolbar class="zotero-toolbar toolbar toolbar-primary">
<hbox align="center">
<toolbarbutton
id="github-link"
class="zotero-tb-button"
data-l10n-id="github-link"
image="chrome://__addonRef__/content/icons/github.svg"
/>
<spacer flex="1"></spacer>
<search-textbox
id="search-box"
data-l10n-id="search-box"
data-l10n-attrs="placeholder"
timeout="1"
style="padding: 0"
/>
</hbox>
</toolbar>
</header>
<main>
<div id="table-container"></div>
</main>
<footer>
<div
id="buttons"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
>
<button
id="request-new-translator"
data-l10n-id="request-new-translator"
/>
<button
id="report-translator-bug"
data-l10n-id="report-translator-bug"
/>
</div>
<div
id="links"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
>
<label
data-l10n-id="how-to-update-translators"
class="zotero-text-link"
is="zotero-text-link"
href="https://zotero-chinese.com/user-guide/faqs/update-translators"
/>
<label
data-l10n-id="translators-dashboard"
class="zotero-text-link"
is="zotero-text-link"
href="https://zotero-chinese.com/translators/"
/>
</div>
</footer>
</body>
</html>
================================================
FILE: addon/chrome/content/prefpanel.css
================================================
:root {
--dropdown-bg-color: #f9f9f9;
--dropdown-text-color: #333;
--dropdown-border-color: #ccc;
--dropdown-shadow-color: rgba(0, 0, 0, 0.2);
}
@media (prefers-color-scheme: dark) {
:root {
--dropdown-bg-color: #333;
--dropdown-text-color: #f9f9f9;
--dropdown-border-color: #555;
--dropdown-shadow-color: rgba(255, 255, 255, 0.2);
}
}
.dropdown-content {
display: none;
position: absolute;
background-color: var(--dropdown-bg-color);
border: 1px solid var(--dropdown-border-color);
box-shadow: 0px 8px 16px var(--dropdown-shadow-color);
z-index: 1;
padding: 8px 16px;
font-size: 12px;
cursor: pointer;
border-radius: 4px;
color: var(--dropdown-text-color);
}
.dropdown-content label {
display: block;
margin: 5px 0;
cursor: pointer;
color: var(--dropdown-text-color);
}
.show {
display: block;
}
.hidden {
display: none;
}
.help-icon {
margin-top: 5px;
margin-bottom: 5px;
width: 20px;
}
================================================
FILE: addon/chrome/content/progress.xhtml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <?xml-stylesheet
href="chrome://zotero/skin/zotero.css" type="text/css"?> <?xml-stylesheet
href="chrome://zotero-platform/content/zotero-react-client.css"
type="text/css"?> <?xml-stylesheet
href="chrome://zotero-platform/content/zotero.css" type="text/css"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
>
<head>
<meta charset="UTF-8" />
<xul:linkset>
<link rel="localization" href="__addonRef__-progress.ftl" />
</xul:linkset>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title data-l10n-id="title"></title>
<style>
/* 默认样式(Light Mode) */
html {
height: 100%;
}
body {
display: flex;
flex-direction: column;
font-family: Arial, sans-serif;
padding: 8px 8px 40px; /* 底部留出按钮空间 */
box-sizing: border-box; /* 确保尺寸计算包含padding */
border-radius: 4px;
background-color: #ffffff;
color: #000000;
height: 90vh; /* 使用视口高度 */
}
h1 {
flex-shrink: 0; /* 禁止标题收缩 */
margin: 0 0 10px 0;
}
/* 可滚动区域 */
#task-list {
flex: 1; /* 占据剩余空间 */
overflow-y: auto; /* 垂直滚动 */
min-height: 0; /* 允许内容压缩 */
}
/* 调整底部按钮定位 */
div.buttons {
position: fixed;
top: 332px;
right: 35px;
background: inherit; /* 继承背景色 */
border-radius: 4px;
}
div.hidden {
display: none;
}
.task {
margin-bottom: 10px;
border: 1px solid #ddd;
padding: 10px;
border-radius: 5px;
background-color: #f9f9f9;
}
.task-header {
display: flex;
align-items: center;
cursor: pointer;
position: relative;
}
.task-status {
margin-right: 10px;
font-size: 20px;
}
.task-title {
font-weight: bold;
flex-grow: 1;
}
.toggle-icon {
margin-left: 10px;
font-size: 14px;
transition: transform 0.2s;
}
.search-results-container {
display: flex;
align-items: flex-start; /* 确保按钮和搜索结果对齐 */
margin-left: 20px;
margin-top: 10px;
}
.confirm-button {
margin-right: 4px; /* 按钮放在搜索结果左侧 */
margin-left: -20px;
padding: 4px 8px;
background: #4caf50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
display: none; /* 默认隐藏 */
font-size: 12px;
width: 50px;
margin-top: 1.5px;
}
.confirm-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.search-results {
flex-grow: 1; /* 搜索结果占据剩余空间 */
}
.search-result {
margin-bottom: 4px;
padding: 4px;
border: 1px solid #eee;
border-radius: 3px;
background-color: #ffffff;
display: flex;
align-items: center;
}
.search-result input[type="radio"] {
margin-right: 8px;
}
.search-result .info {
flex-grow: 1;
}
.search-result .source {
color: #666;
font-size: 0.9em;
}
.search-result .title {
font-weight: bold;
}
.task.completed .search-result input[type="radio"] {
pointer-events: none; /* 禁用单选按钮 */
opacity: 0.5; /* 降低透明度 */
}
.task-msg {
width: 15px;
vertical-align: middle;
cursor: pointer;
display: inline-block;
animation: attention-shake 2s ease-in-out infinite;
}
/* 吸引注意力的动画:抖动 + 缩放 + 脉冲 */
@keyframes attention-shake {
0%,
100% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
10% {
transform: scale(1.3) rotate(-10deg);
}
20% {
transform: scale(1.3) rotate(10deg);
}
30% {
transform: scale(1.3) rotate(-10deg);
}
40% {
transform: scale(1.3) rotate(10deg);
}
50% {
transform: scale(1.5) rotate(0deg);
opacity: 0.8;
}
60% {
transform: scale(1.2) rotate(0deg);
}
70% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
}
/* Wrapper for icon + popover hover area */
.task-msg-wrapper {
position: relative;
display: inline-block;
margin-left: 8px;
vertical-align: middle;
}
/* 鼠标悬停时暂停动画并放大 */
.task-msg-wrapper:hover .task-msg,
.task-msg.active {
animation-play-state: paused;
transform: scale(1.5);
filter: drop-shadow(0 0 3px rgba(255, 193, 7, 0.8));
}
/* Custom popover for task messages */
.task-msg-popover {
position: absolute;
left: 0;
top: calc(100% + 8px);
padding: 12px 16px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 10px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.12),
0 2px 8px rgba(0, 0, 0, 0.06);
font-size: 12px;
font-weight: normal;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-all;
max-width: 420px;
min-width: 240px;
max-height: 250px;
overflow-y: auto;
z-index: 100;
color: #1d1d1f;
/* Hover transition */
opacity: 0;
visibility: hidden;
transform: translateY(4px);
transition:
opacity 0.2s ease,
visibility 0.2s ease,
transform 0.2s ease;
pointer-events: none;
}
/* Arrow */
.task-msg-popover::before {
content: "";
position: absolute;
top: -6px;
left: 12px;
width: 12px;
height: 12px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border-top: 1px solid rgba(0, 0, 0, 0.08);
border-left: 1px solid rgba(0, 0, 0, 0.08);
transform: rotate(45deg);
}
.task-msg-popover.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
pointer-events: auto;
}
.task-msg-popover a {
color: #0066cc;
text-decoration: none;
border-bottom: 1px solid rgba(0, 102, 204, 0.3);
transition:
border-color 0.15s ease,
color 0.15s ease;
cursor: pointer;
}
.task-msg-popover a:hover {
color: #0047ab;
border-bottom-color: rgba(0, 71, 171, 0.6);
}
/* 黑暗模式样式 */
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
color: #e0e0e0;
}
.task {
background-color: #1e1e1e;
border-color: #444;
}
.search-result,
div.buttons {
background-color: #2d2d2d;
border-color: #444;
}
.search-result .source {
color: #999;
}
.search-result .title {
color: #e0e0e0;
}
.task-status {
color: #e0e0e0;
}
.task-title,
div.buttons {
color: #e0e0e0;
}
.toggle-icon {
color: #e0e0e0;
}
.confirm-button {
background-color: #4caf50;
}
.confirm-button:disabled {
background-color: #666;
}
.task-msg-popover {
background: rgba(40, 40, 40, 0.85);
border-color: rgba(255, 255, 255, 0.1);
color: #e0e0e0;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
0 2px 8px rgba(0, 0, 0, 0.2);
}
.task-msg-popover::before {
background: rgba(40, 40, 40, 0.85);
border-top-color: rgba(255, 255, 255, 0.1);
border-left-color: rgba(255, 255, 255, 0.1);
}
.task-msg-popover a {
color: #8ab4f8;
border-bottom-color: rgba(138, 180, 248, 0.3);
}
.task-msg-popover a:hover {
color: #aecbfa;
border-bottom-color: rgba(174, 203, 250, 0.6);
}
}
</style>
</head>
<body>
<h1 data-l10n-id="task-list"></h1>
<div class="hidden">
<p id="msg1" data-l10n-id="confirm-close"></p>
</div>
<div id="task-list"></div>
<div class="buttons">
<button id="button-cancel">Close</button>
<button id="button-ok" style="display: none">OK</button>
</div>
</body>
<script>
//<![CDATA[
if (window.arguments) {
document.addEventListener("DOMContentLoaded", (ev) => {
Services.scriptloader.loadSubScript(
"chrome://zotero/content/include.js",
this,
);
window.arguments[0]._initPromise.resolve();
});
}
// window.addEventListener("beforeunload", (e) => {
// Zotero.Jasminum.data.myCookieSandbox._CNKIHomeCookieBox = null;
// });
// 模拟数据
const tasks = [
{
id: "1",
type: "attachment",
item: { getField: () => "论文标题 1" },
status: "success",
message: "This is error msg1",
searchResult: [
{
source: "Source A",
title: "Result 1",
url: "https://example.com/1",
},
{
source: "Source B",
title: "Result 2",
url: "https://example.com/2",
},
],
},
{
id: "2",
type: "snapshot",
item: { getField: () => "论文标题 2" },
status: "processing",
message: "This is error msg2",
},
{
id: "3",
type: "attachment",
item: { getField: () => "论文标题 3" },
status: "fail",
message:
"抓取失败\n[小红书l0o0](https://www.xiaohongshu.com/user/profile/6153b4fa000000001f03ac8c)",
},
];
// 状态图标映射
const statusIcons = {
waiting: "⏳",
processing: "🔄",
multiple_results: "🔍",
success: "✅",
fail: "❌",
};
// Convert [text](url) and bare URLs to clickable links
function linkifyText(text) {
return text
.split("\n")
.map((line) =>
line
.replace(
/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g,
'<a href="#" data-url="$2">$1</a>',
)
.replace(
/(https?:\/\/[^\s<]+)(?![^<]*<\/a>)/g,
'<a href="#" data-url="$1">$1</a>',
),
)
.join("<br>");
}
// 渲染任务列表
function renderTaskList(tasks) {
const taskList = document.getElementById("task-list");
if (!taskList) return;
taskList.innerHTML = tasks
.map(
(task) => `
<div class="task" data-task-id="${task.id}">
<div class="task-header">
<span class="task-status">${statusIcons[task.status]}</span>
<span class="task-title">${task.item.getField("title")}
<span class="task-msg-wrapper">
<span class="task-msg" id="task-msg-${task.id}">⚠️</span>
<div class="task-msg-popover" id="task-msg-popover-${task.id}">${linkifyText(task.message)}</div>
</span>
</span>
${
task.searchResult && task.searchResult.length > 0
? `<span class="toggle-icon" id="toggle-icon-${task.id}">▼</span>`
: ""
}
</div>
${
task.searchResult && task.searchResult.length > 0
? `
<div class="search-results-container" id="search-results-container-${task.id}">
<button class="confirm-button" data-task-id="${task.id}">确认</button>
<div class="search-results" id="search-results-${task.id}">
${task.searchResult
.map(
(result, index) => `
<div class="search-result">
<input type="radio" name="task-${task.id}" data-task-id="${task.id}" data-result-index="${index}" />
<div class="info">
<span class="source" data-l10n-id="result-source" data-l10n-args='{"source": "${result.source}"}'></span>
<span class="title" data-l10n-id="result-title" data-l10n-args='{"title": "${result.title}"}'></span>
<span class="score" data-l10n-id="result-score" data-l10n-args='{"score": "${result.score}"}'></span>
</div>
</div>
`,
)
.join("")}
</div>
</div>
`
: ""
}
</div>
`,
)
.join("");
}
// 切换搜索结果的显示/隐藏
function toggleSearchResults(taskId) {
const searchResultsContainer = document.getElementById(
`search-results-container-${taskId}`,
);
const toggleIcon = document.getElementById(`toggle-icon-${taskId}`);
console.log("click", `#search-results-container-${taskId}`);
if (searchResultsContainer && toggleIcon) {
if (searchResultsContainer.style.display === "none") {
searchResultsContainer.style.display = "";
toggleIcon.textContent = "▲"; // 展开时显示向上箭头
} else {
searchResultsContainer.style.display = "none";
toggleIcon.textContent = "▼"; // 收起时显示向下箭头
}
}
}
// Close all open popovers
function closeAllPopovers() {
document.querySelectorAll(".task-msg-popover.visible").forEach((p) => {
p.classList.remove("visible");
});
document.querySelectorAll(".task-msg.active").forEach((i) => {
i.classList.remove("active");
});
}
// Show popover on hover, close on click outside
document.getElementById("task-list").addEventListener(
"mouseenter",
(event) => {
const wrapper = event.target.closest(".task-msg-wrapper");
if (!wrapper) return;
closeAllPopovers();
const popover = wrapper.querySelector(".task-msg-popover");
const icon = wrapper.querySelector(".task-msg");
if (popover) popover.classList.add("visible");
if (icon) icon.classList.add("active");
},
true,
);
document.addEventListener("click", (event) => {
if (
!event.target.closest(".task-msg-popover") &&
!event.target.closest(".task-msg-wrapper")
) {
closeAllPopovers();
}
});
// 事件委托:绑定点击事件
document.getElementById("task-list").addEventListener("click", (event) => {
console.log("click", event.target);
// Handle popover link clicks via Zotero.launchURL
const link = event.target.closest(".task-msg-popover a[data-url]");
if (link) {
event.preventDefault();
event.stopPropagation();
const url = link.getAttribute("data-url");
if (url) {
if (typeof Zotero !== "undefined") {
Zotero.launchURL(url);
} else {
window.open(url, "_blank");
}
}
return;
}
const taskHeader = event.target.closest(".task-header");
if (taskHeader) {
const taskId = taskHeader.closest(".task").getAttribute("data-task-id");
toggleSearchResults(taskId);
}
const radio = event.target.closest('input[type="radio"]');
if (radio) {
const taskId = radio.getAttribute("data-task-id");
const confirmButton = document.querySelector(
`.confirm-button[data-task-id="${taskId}"]`,
);
if (confirmButton) {
confirmButton.style.display = radio.checked ? "inline-block" : "none";
}
}
const confirmButton = event.target.closest(".confirm-button");
if (confirmButton) {
const taskId = confirmButton.getAttribute("data-task-id");
const selectedRadio = document.querySelector(
`input[type="radio"][data-task-id="${taskId}"]:checked`,
);
if (selectedRadio) {
const resultIndex = selectedRadio.getAttribute("data-result-index");
console.log(`已确认选择:${taskId} (${resultIndex})`);
Zotero.Jasminum.taskRunner.resumeTask(taskId, resultIndex);
// 标记任务为已完成
const taskElement = confirmButton.closest(".task");
taskElement.classList.add("completed");
// 禁用所有单选按钮
const radios = taskElement.querySelectorAll('input[type="radio"]');
radios.forEach((radio) => {
radio.disabled = true;
});
// 隐藏确认按钮
confirmButton.style.display = "none";
}
}
});
document.getElementById("button-cancel").addEventListener("click", (e) => {
console.log(e);
const unfinishedTasks = Zotero.Jasminum.taskRunner.tasks.filter(
(t) => t.status != "fail" && t.status != "success",
);
if (unfinishedTasks.length > 0) {
const msg = document
.getElementById("msg1")
.textContent.replace("xxx", unfinishedTasks.length);
const userConfirmed = confirm(msg);
if (userConfirmed) {
window.close();
}
} else {
window.close();
}
});
// 初始化渲染
if (window.arguments == undefined) {
renderTaskList(tasks);
// 默认展开有多个搜索结果的任务
tasks.forEach((task) => {
if (task.searchResult && task.searchResult.length > 0) {
const searchResults = document.getElementById(
`search-results-${task.id}`,
);
const toggleIcon = document.getElementById(`toggle-icon-${task.id}`);
if (searchResults && toggleIcon) {
searchResults.style.display = "block"; // 默认展开
toggleIcon.textContent = "▲"; // 默认显示向上箭头
}
}
});
}
// window.addEventListener("DOMWindowClose", (e) => {
// const shouldClose = confirm("确定要关闭窗口吗?");
// if (!shouldClose) {
// e.preventDefault(); // 阻止关闭
// }
// });
//]]>
</script>
</html>
================================================
FILE: addon/locale/en-US/addon.ftl
================================================
plugin-name = Jasminum
prefs-table-title = Title
prefs-table-detail = Detail
tabpanel-lib-tab-label = Lib Tab
tabpanel-reader-tab-label = Reader Tab
# Preference
select-download-folder = Select download folder
get-Chinese-styles = Get Chinese Styles
info-translators-cn-updaing = Chinese translators are under updating.
info-best-speed-source-updated = Updated to fastest source: { $source }
info-best-speed-source-failed = Failed to select fastest source, please check network connection
# Preference translator table
th-filename = File name
th-label = Label
th-local-update-time = Local update time
th-remote-update-time = Remote update time
# Help menu
help-menu-chinese = Zotero Chinese Community
help-menu-wiki = Zotero Wiki
help-menu-addons = Addon Store
help-menu-csl = Donwload more CSL
help-menu-translator = Help with Chinese literature capture
# Menu
menu-metadata = Metadata(CN)
menuitem-retrieveMetadata = Find article metadata
menuitem-retrieveMetadataForBook = Find book metadata
menuitem-find-attachment = Find attachment in Folder
menuitem-import-attachments = Import attachments from Folder
menu-tools = Tools
menuitem-mergeName = Concat Name
menuitem-splitName = Split Name
menuitem-updateCNKICite = Update CNKI citation
# ui
CNKIcitation = CNKICite
# popup window
citation = Cite
no-chinese-item-for-citation = Only Chinese items can find CNKI citation
update-translators-start = Start updating translators
update-successfully = Update successfully: { $name }
update-failed = Update failed: { $name }
update-skipped = Up to date: { $name }
update-translators-complete = Update translators completed, Success: { $successCounts }, Failed: { $failCounts }, Up to date: { $skipCounts }
no-item-need-attachment = No item need attachment
no-attachments-found = No attachments found (PDF, CAJ, etc.)
import-attachments-success = Import attachments from folder successfully
importing-attachments-is-running = An attachment import task is already running. Please try again later.
# outline
outline = Show Bookmark (By Jasminum)
outline-expand-all = Expand all
outline-collapse-all = Collapse all
outline-add = Add bookmark
outline-delete = Delete bookmark
outline-save-to-pdf = Save outline to PDF
outline-empty-prompt = Please click the button above { $icon } to create a bookmark
outline-delete-confirm = This node has child nodes, do you want to delete?
{" "}
If you delete, all child nodes will be deleted.
# bookmark
bookmark = Show Bookmarks (By Jasminum)
bookmark-add = Add bookmark
bookmark-delete = Delete bookmark
# Progress window
task-msg-header = If you need help with capture issues, please screenshot the following content and contact the developer: [RedBook l0o0](https://www.xiaohongshu.com/user/profile/6153b4fa000000001f03ac8c)
task-already-exists = Task already exists: { $title }
================================================
FILE: addon/locale/en-US/mainWindow.ftl
================================================
item-section-example1-head-text =
.label = Plugin Template: Item Info
item-section-example1-sidenav-tooltip =
.tooltiptext = This is Plugin Template section (item info)
item-section-example2-head-text =
.label = Plugin Template: Reader [{$status}]
item-section-example2-sidenav-tooltip =
.tooltiptext = This is Plugin Template section (reader)
item-section-example2-button-tooltip =
.tooltiptext = Unregister this section
================================================
FILE: addon/locale/en-US/preferences-main.ftl
================================================
# Metadata Settings
pref-group-metadata = Chinese Metadata Retrieval Settings
label-isMainlandChina =
.label = Currently located in Chinese Mainland (excluding Hong Kong, Macao and Taiwan), uncheck for overseas users
label-autoupdate-metadata =
.label = Automatically retrieve metadata from CNKI when adding Chinese PDF/CAJ files
label-rename =
.label = Rename attachments based on metadata (requires Attanger or zotmoov plugin)
label-namepattern = Filename Parsing Template
label-namepattern-auto =
.label = Smart Recognition
.tooltiptext = Use Jasmine's built-in algorithm to intelligently identify authors or titles from filenames
label-namepattern-tg =
.label = Title_Author (Default)
.tooltiptext = Rename files in the format "Title_FirstAuthor," e.g., "Design and Application of Redundant Avionics Systems for Drones_Yang Lu.caj"
label-namepattern-t =
.label = Title
.tooltiptext = Rename files using the "Title" format, e.g., "Design and Application of Redundant Avionics Systems for Drones.caj"
label-namepattern-info = Filename recognition template. Select a format from the dropdown or enter directly
label-namepattern-custom =
.label = Custom
.tooltiptext = Set custom rules to extract title and author information from filenames for metadata retrieval
label-choose-namepattern =
.label = Select Template
label-metadata-source = Metadata Retrieval Source
label-choose-source =
.label = Select Data Source
label-metadata-source-cnki =
.label = CNKI (China National Knowledge Infrastructure)
label-metadata-source-cvip =
.label = VIP Journals (Chinese VIP Information)
label-pdf-match-folder = Attachment Matching Folder
label-choose-folder =
.label = Select Folder
namepattern-desc =
.tooltiptext = Retrieve CNKI metadata based on filenames. Filename format settings: {"{"}%t{"}"}=Title, {"{"}%g{"}"}=Author, {"{"}%y{"}"}=Year, {"{"}%j{"}"}=Other (e.g., source information); specify separators as needed; multiple separators can be used consecutively; file extensions are ignored. Default uses {"{"}%t{"}"}_{"{"}%g{"}"}, which recognizes most CNKI filename formats, including filenames with only titles and no separators.
# Transator Settings
pref-group-translators = Chinese Translator Settings
label-translator-source = Translator Download Source
label-best-speed = Choose Fastest Source
translatorSource-desc =
.tooltiptext = Select the translator download source. Generally, there is no need to switch. If you cannot download the Chinese translator, you can try other sources or click the "Choose Fastest Source" button.
label-auto-update-translators =
.label = Automatically Update Translators
label-translators-force-update =
.label = Update Immediately
label-translators-detail = Translator Details
label-translators-detail-click = Click to View
# Attachment Settings
pref-group-attachment = Local Attachment Search Settings
attachment-folder-desc =
.tooltiptext = Search for attachments in the download directory and match them to entries that are missing attachments.
Set this to your browser's download directory, and the plugin can batch import and search for attachments from the download directory.
label-pdf-match-folder = Attachment Download Folder
action-after-import = After matching attachments to entries, what to do with the original downloaded files:
label-choose-folder =
.label = Choose Folder
nothing-label =
.label = Do Nothing
backup-label =
.label = Backup Attachment
delete-label =
.label = Delete Attachment
action-after-import-desc =
.tooltiptext = After successfully matching attachments to entries, you can choose one of the following actions:
1. Do Nothing: No action is taken, and the downloaded files remain in the download directory.
2. Backup Attachment: Back up the original downloaded files to a specified directory.
3. Delete Attachment: Delete the original downloaded files (the attachments are already matched and saved in Zotero).
# Outline Bookmark Settings
pref-group-bookmark = Outline Bookmark Settings
label-disableZoteroOutline =
.label = Disable Zotero's Built-in Outline
label-enableBookmark =
.label = Enable Outline Bookmark
outline-desc =
.tooltiptext = Please note that when you modify the outline or bookmarks, you need to click the 'Save' button to save them to the PDF file. By default, bookmark and outline information is saved separately from the PDF file.
# Tool Settings
pref-group-tools = Tool Settings
label-auto-split-name =
.label = Automatically split first name and last name when adding new items
label-split-en-name =
.label = Include English names when splitting/merging names
label-language = Manually Set Language
label-tools-info-1 = 💡 The
label-tools-info-2 = provides richer metadata inspection functionality
label-tools-linter = Linter Plugin
# WPS Plugin Installation
pref-group-wps = WPS Zotero Plugin
label-wps = Install Zotero Add-on for WPS
label-wps-help = Usage Help
label-install-wps-plugin-click =
.label = Click to Install
# About
pref-group-about = About
pref-help = Version { $version } Build { $time } ❤️
label-zotero-chinese = Zotero Chinese Community
pref-enable =
.label = Enable
================================================
FILE: addon/locale/en-US/preferences-translators.ftl
================================================
title = Chinese Community Translators List
github-link =
.label = Project Homepage
search-box =
.placeholder = Search translators
# Links
how-to-update-translators = How to update translators?
translators-dashboard = Translators Dashboard
# Buttons
request-new-translator = Request new translator
report-translator-bug = Report translator bug
================================================
FILE: addon/locale/en-US/progress.ftl
================================================
title = Jasmine Task Window
task-list = Task List
result-source = Source: { source }
result-title = Title: { title }
result-score = Match Score: { score }
confirm-close = There are still xxx pending tasks. Close the window anyway?
================================================
FILE: addon/locale/zh-CN/addon.ftl
================================================
plugin-name = 茉莉花
prefs-table-title = 标题
prefs-table-detail = 详情
tabpanel-lib-tab-label = 库标签
tabpanel-reader-tab-label = 阅读器标签
# Preference
select-download-folder = 选择下载文件保存目录
get-Chinese-styles = 获取中文社区样式…
info-translators-cn-updaing = 中文转换器正在更新中
info-best-speed-source-updated = 已更新为最快源:{ $source }
info-best-speed-source-failed = 选择最快源失败,请检查网络连接
# Preference translator table
th-filename = 文件名
th-label = 标签
th-local-update-time = 本地更新时间
th-remote-update-time = 远程更新时间
# Help menu
help-menu-chinese = Zotero 中文社区
help-menu-wiki = Zotero 使用文档
help-menu-addons = 插件商店
help-menu-csl = CSL样式下载
help-menu-translator = 中文文献抓取异常修复
# Menu
menu-metadata = 茉莉花抓取
menuitem-retrieveMetadata = 抓取期刊元数据
menuitem-retrieveMetadataForBook = 抓取书籍元数据
menuitem-find-attachment = 在下载文件夹中查找附件
menuitem-import-attachments = 从下载文件夹中导入附件
menu-tools = 小工具
menuitem-mergeName = 合并姓名
menuitem-splitName = 拆分姓名
menuitem-updateCNKICite = 更新知网引用数
# ui
CNKIcitation = 知网引用数
# popup window
citation = 引用
no-chinese-item-for-citation = 只有中文条目才能抓取引用数哦😀
update-translators-start = 开始更新转换器
update-successfully = 更新成功:{ $name }
update-failed = 更新失败:{ $name }
update-skipped = 已最新:{ $name }
update-translators-complete = 转换器更新完成,成功:{ $successCounts }, 失败:{ $failCounts }, 已最新:{ $skipCounts }
no-item-need-attachment = 这些条目已有附件或属于非学术类型条目
no-attachments-found = 未找到可导入的附件(PDF, CAJ等)
import-attachments-success = 从文件夹中导入附件成功
importing-attachments-is-running = 已有一个附件导入任务正在运行,请稍后再试。
# outline
outline = 显示书签大纲(茉莉花)
outline-expand-all = 展开所有
outline-collapse-all = 收起所有
outline-add = 添加书签
outline-delete = 删除书签
outline-save-to-pdf = 将大纲保存到PDF文件
outline-edit-placeholder = 请输入书签
outline-empty-prompt = 请点击上方按钮{ $icon }创建书签
outline-delete-confirm = 该节点有子节点,是否删除?
{" "}
如果删除,则所有子节点也会被删除。
# bookmark
bookmark = 显示书签(茉莉花)
bookmark-add = 添加书签
bookmark-delete = 删除书签
# Progress window
task-msg-header = 如果抓取异常需要帮助,请截图以下内容并联系开发者:[小红书l0o0](https://www.xiaohongshu.com/user/profile/6153b4fa000000001f03ac8c)
task-already-exists = 任务已存在:{ $title }
================================================
FILE: addon/locale/zh-CN/mainWindow.ftl
================================================
item-section-example1-head-text =
.label = 插件模板: 条目信息
item-section-example1-sidenav-tooltip =
.tooltiptext = 这是插件模板面板(条目信息)
item-section-example2-head-text =
.label = 插件模板: 阅读器[{$status}]
item-section-example2-sidenav-tooltip =
.tooltiptext = 这是插件模板面板(阅读器)
item-section-example2-button-tooltip =
.tooltiptext = 移除此面板
================================================
FILE: addon/locale/zh-CN/preferences-main.ftl
================================================
# 元数据设置
pref-group-metadata = 中文元数据抓取设置
label-isMainlandChina =
.label = 当前位于中国大陆(不包括中国香港、中国澳门及中国台湾),海外用户请取消勾选
label-autoupdate-metadata =
.label = 添加中文PDF/CAJ时自动从知网抓取元数据
label-rename =
.label = 根据元数据重命名附件(依赖Attanger或zotmoov插件)
label-namepattern = 文件名解析模板
label-namepattern-auto =
.label = 智能识别
.tooltiptext = 利用茉莉花内置的算法智能识别文件名的作者或标题
label-namepattern-tg =
.label = 标题_作者(默认设置)
.tooltiptext =「标题_第一作者」格式命名文件,如「无人机多余度航空电子系统设计与应用_杨璐.caj」
label-namepattern-t =
.label = 标题
.tooltiptext =「标题」格式命名文件,如「无人机多余度航空电子系统设计与应用.caj」
label-namepattern-info = 文件名识别模板,从下拉菜单中选择对应格式或直接输入
label-namepattern-custom =
.label = 自定义
.tooltiptext = 设置自定义规则,识别文件名中的标题、作者信息用于元数据抓取
label-choose-namepattern =
.label = 选择模板
label-metadata-source = 元数据抓取来源
label-choose-source =
.label = 选择数据源
label-metadata-source-cnki =
.label = 中国知网CNKI
label-metadata-source-cvip =
.label = 维普期刊CVIP
namepattern-desc =
.tooltiptext = 根据文件名抓取知网元数据,文件名格式设置:
{"{"}%t{"}"}=标题,{"{"}%g{"}"}=作者,{"{"}%y{"}"}=年份,{"{"}%j{"}"}=其他(例如来源信息);分隔符依实情指定,可连续使用多个;不用考虑文件后缀名。
默认使用{"{"}%t{"}"}_{"{"}%g{"}"},可识别大部分知网下载的文件名格式,包括文件名只包括标题无分隔符号。
# 附件查找设置
pref-group-attachment = 本地附件查找设置
attachment-folder-desc =
.tooltiptext = 从下载目录中查找附件,并匹配到缺少附件的条目中。
此处请设置为浏览器的下载目录,插件就可以批量从下载目录中导入和查询附件。
label-pdf-match-folder = 附件下载文件夹
action-after-import = 附件匹配到条目之后,如何处理原始下载的附件文件:
label-choose-folder =
.label = 选择文件夹
nothing-label =
.label = 无须处理
backup-label =
.label = 备份附件
delete-label =
.label = 删除附件
action-after-import-desc =
.tooltiptext = 附件成功匹配到条目之后,您可以选择以下操作:
1. 无须处理:不做任何操作,下载的附件还在下载目录中;
2. 备份附件:将原始下载的附件文件备份到指定目录;
3. 删除附件:删除原始下载的附件文件,该附件已经匹配到条目,保存到Zotero中,可放心删除。
# 转换器设置
pref-group-translators = 中文转换器设置
label-translator-source = 转换器下载源
label-best-speed = 选择最快源
translatorSource-desc =
.tooltiptext = 选择转换器下载源,一般情况下不用切换。如果您无法下载中文转换器,可选择尝试其他源或点击 选择最快源 按钮。
label-auto-update-translators =
.label = 自动更新转换器
label-translators-force-update =
.label = 立即更新
label-translators-detail = 转换器详情
label-translators-detail-click = 点击查看
# 大纲书签设置
pref-group-bookmark = 大纲书签设置
label-disableZoteroOutline =
.label = 禁用 Zotero 自带的大纲
label-enableBookmark =
.label = 启用大纲书签
outline-desc =
.tooltiptext = 请注意,当您修改大纲或书签时,需要点击保存按钮才会保存到PDF文件中。默认将书签大纲信息与PDF文件分开保存。
# 小工具设置
pref-group-tools = 小工具设置
label-auto-split-name =
.label = 导入新条目时自动拆分姓名
label-split-en-name =
.label = 拆分/合并姓名时包括英文名
label-language = 手动设置语言
label-tools-info-1 = 💡
label-tools-info-2 = 提供更丰富的元数据检查功能
label-tools-linter = Linter 插件
# WPS 插件安装
pref-group-wps = WPS Zotero 插件
label-wps = 为 WPS 安装 Zotero 加载项
label-wps-help = 使用帮助
label-install-wps-plugin-click =
.label = 点击安装
# 其他
pref-group-about = 关于
pref-help = 版本 { $version } 构建于 { $time } ❤️
label-zotero-chinese = Zotero中文社区
pref-enable =
.label = 启用
================================================
FILE: addon/locale/zh-CN/preferences-translators.ftl
================================================
title = 中文社区转换器列表
github-link =
.label = 项目主页
search-box =
.placeholder = 搜索转换器
# Links
how-to-update-translators = 如何更新转换器?
translators-dashboard = 转换器看板
# Buttons
request-new-translator = 申请适配
report-translator-bug = 反馈错误
================================================
FILE: addon/locale/zh-CN/progress.ftl
================================================
title = 茉莉花任务窗口
task-list = 任务列表
result-source = 来源:{ source }
result-title = 标题:{ title }
result-score = 匹配度:{ score }
confirm-close = 还有 xxx 个任务未完成,是否关闭窗口?
================================================
FILE: addon/locale/zh-TW/addon.ftl
================================================
plugin-name = 茉莉花
prefs-table-title = 標題
prefs-table-detail = 詳細資料
tabpanel-lib-tab-label = 圖書館標籤
tabpanel-reader-tab-label = 閱讀器標籤
# Preference
select-download-folder = 選擇下載檔案儲存目錄
get-Chinese-styles = 取得中文社群樣式…
info-translators-cn-updaing = 中文轉換器正在更新中
info-best-speed-source-updated = 已更新為最快源:{ $source }
info-best-speed-source-failed = 選擇最快源失敗,請檢查網路連接
# Preference translator table
th-filename = 檔案名稱
th-label = 標籤
th-local-update-time = 本地更新時間
th-remote-update-time = 遠程更新時間
# Help menu
help-menu-chinese = Zotero 中文社群
help-menu-wiki = Zotero 使用說明
help-menu-addons = 插件商店
help-menu-csl = CSL樣式下載
help-menu-translator = 中文文獻抓取異常解決
# Menu
menu-metadata = 元資料抓取
menuitem-retrieveMetadata = 抓取期刊元資料
menuitem-retrieveMetadataForBook = 抓取書籍元資料
menuitem-find-attachment = 在資料夾中尋找附件
menuitem-import-attachments = 從資料夾中導入附件
menu-tools = 小工具
menuitem-mergeName = 合併姓名
menuitem-splitName = 拆分姓名
menuitem-updateCNKICite = 更新知網引用數
# ui
CNKIcitation = 知網引用數
# popup window
citation = 引用
no-chinese-item-for-citation = 只有中文項目才能抓取引用數哦😀
update-translators-start = 開始更新轉換器
update-successfully = 更新成功:{ $name }
update-failed = 更新失敗:{ $name }
update-skipped = 已最新:{ $name }
update-translators-complete = 轉換器更新完成,成功:{ $successCounts }, 失敗:{ $failCounts }, 已最新:{ $skipCounts }
no-item-need-attachment = 項目已存在附件或屬於非學術類型
import-attachments-success = 從資料夾中導入附件成功
importing-attachments-is-running = 已有附件匯入任務正在執行,請稍後再試
no-attachments-found = 未找到可匯入的附件(PDF、CAJ等)
# outline
outline = 顯示書籤大纲(茉莉花)
outline-expand-all = 展開所有
outline-collapse-all = 收起所有
outline-add = 添加書籤
outline-delete = 刪除書籤
outline-save-to-pdf = 將大纲儲存到PDF檔案
outline-edit-placeholder = 請輸入書籤
outline-empty-prompt = 請點擊上方按鈕{ $icon }創建書籤
outline-delete-confirm = 該節點有子節點,是否刪除?
{" "}
如果刪除,則所有子節點也會被刪除。
# bookmark
bookmark = 顯示書籤(茉莉花)
bookmark-add = 添加書籤
bookmark-delete = 刪除書籤
# Progress window
task-msg-header = 如果抓取異常需要幫助,請截圖以下內容並聯繫開發者:[小紅書l0o0](https://www.xiaohongshu.com/user/profile/6153b4fa000000001f03ac8c)
task-already-exists = 已存在任務:{ $title }
================================================
FILE: addon/locale/zh-TW/mainWindow.ftl
================================================
item-section-example1-head-text =
.label = 插件模板: 条目信息
item-section-example1-sidenav-tooltip =
.tooltiptext = 这是插件模板面板(条目信息)
item-section-example2-head-text =
.label = 插件模板: 阅读器[{$status}]
item-section-example2-sidenav-tooltip =
.tooltiptext = 这是插件模板面板(阅读器)
item-section-example2-button-tooltip =
.tooltiptext = 移除此面板
================================================
FILE: addon/locale/zh-TW/preferences-main.ftl
================================================
# 元數據設定
pref-group-metadata = 中文元數據抓取設定
label-isMainlandChina =
.label = 目前位於中國大陸(不包括中國香港、中國澳門及中國台灣),海外用戶請取消勾選
label-autoupdate-metadata =
.label = 新增中文PDF/CAJ時自動從知網抓取元數據
label-rename =
.label = 根據元數據重新命名附件(依賴Attanger或zotmoov插件)
label-namepattern = 檔案名稱解析範本
label-namepattern-auto =
.label = 智能識別
.tooltiptext = 利用茉莉花內建的演算法智能識別檔案名稱的作者或標題
label-namepattern-tg =
.label = 標題_作者(預設設定)
.tooltiptext =「標題_第一作者」格式命名檔案,如「無人機多餘度航空電子系統設計與應用_楊璐.caj」
label-namepattern-t =
.label = 標題
.tooltiptext =「標題」格式命名檔案,如「無人機多餘度航空電子系統設計與應用.caj」
label-namepattern-info = 檔案名稱識別範本,從下拉選單中選擇對應格式或直接輸入
label-namepattern-custom =
.label = 自訂
.tooltiptext = 設定自訂規則,識別檔案名稱中的標題、作者資訊用於元數據抓取
label-choose-namepattern =
.label = 選擇範本
label-metadata-source = 元數據抓取來源
label-choose-source =
.label = 選擇資料來源
label-metadata-source-cnki =
.label = 中國知網CNKI
label-metadata-source-cvip =
.label = 維普期刊CVIP
label-pdf-match-folder = 附件匹配資料夾
label-choose-folder =
.label = 選擇資料夾
namepattern-desc =
.tooltiptext = 根據檔案名稱抓取知網元數據,檔案名稱格式設定:{"{"}%t{"}"}=標題,{"{"}%g{"}"}=作者,{"{"}%y{"}"}=年份,{"{"}%j{"}"}=其他(例如來源資訊);分隔符依實際情況指定,可連續使用多個;不用考慮檔案副檔名。預設使用{"{"}%t{"}"}_{"{"}%g{"}"},可識別大部分知網下載的檔案名稱格式,包括檔案名稱只包括標題無分隔符號。
# 轉換器設定
pref-group-translators = 中文轉換器設定
label-translator-source = 轉換器下載源
label-best-speed = 選擇最快源
translatorSource-desc =
.tooltiptext = 選擇轉換器下載源,一般情況下不用切換。如果您無法下載中文轉換器,可選擇嘗試其他源或點擊「選擇最快源」按鈕。
label-auto-update-translators =
.label = 自動更新轉換器
label-translators-force-update =
.label = 立即更新
label-translators-detail = 轉換器詳情
label-translators-detail-click = 點擊查看
# 附件設定
pref-group-attachment = 本地附件查找設定
attachment-folder-desc =
.tooltiptext = 從下載目錄中查找附件,並匹配到缺少附件的條目中
此處請設定為瀏覽器的下載目錄,插件即可批次從下載目錄中匯入及查詢附件。
label-pdf-match-folder = 附件下載資料夾
action-after-import = 附件匹配到條目之後,如何處理原始下載的附件檔案:
label-choose-folder =
.label = 選擇資料夾
nothing-label =
.label = 無須處理
backup-label =
.label = 備份附件
delete-label =
.label = 刪除附件
action-after-import-desc =
.tooltiptext = 附件成功匹配到條目之後,您可以選擇以下操作:
1. 無須處理:不做任何操作,下載的附件仍保留在下載目錄中;
2. 備份附件:將原始下載的附件檔案備份到指定目錄;
3. 刪除附件:刪除原始下載的附件檔案(該附件已匹配到條目並儲存到Zotero中)。
# 大綱書籤設定
pref-group-bookmark = 大綱書籤設定
label-disableZoteroOutline =
.label = 禁用 Zotero 自帶的大綱
label-enableBookmark =
.label = 啟用大綱書籤
outline-desc =
.tooltiptext = 請注意,當您修改大綱或書籤時,需要點擊「儲存」按鈕才會將變更保存至PDF檔案中。預設情況下,書籤與大綱資訊會與PDF檔案分開儲存。
# 小工具設定
pref-group-tools = 小工具設定
label-auto-split-name =
.label = 導入新條目時自動拆分姓名
label-split-en-name =
.label = 拆分/合併姓名時包括英文名
label-language = 手動設定語言
label-tools-info-1 = 💡
label-tools-info-2 = 提供更豐富的元數據檢查功能
label-tools-linter = Linter 插件
# WPS 插件安裝
pref-group-wps = WPS Zotero 插件
label-wps = 為 WPS 安裝 Zotero 加載項
label-wps-help = 使用說明
label-install-wps-plugin-click =
.label = 點擊安裝
# 其他
pref-group-about = 關於
pref-help = 版本 { $version } 建置於 { $time } ❤️
label-zotero-chinese = Zotero中文社群
pref-enable =
.label = 啟用
================================================
FILE: addon/locale/zh-TW/preferences-translators.ftl
================================================
title = 中文社群轉換器列表
github-link =
.label = 專案首頁
search-box =
.placeholder = 搜尋轉換器
# Links
how-to-update-translators = 如何更新轉換器?
translators-dashboard = 轉換器看板
# Buttons
request-new-translator = 請求适配
report-translator-bug = 報告錯誤
================================================
FILE: addon/locale/zh-TW/progress.ftl
================================================
title = 茉莉花任務視窗
task-list = 任務列表
result-source = 來源:{ source }
result-title = 標題:{ title }
result-score = 匹配度:{ score }
confirm-close = 仍有 xxx 個抓取任務未完成,是否關閉視窗?
================================================
FILE: addon/manifest.json
================================================
{
"manifest_version": 2,
"name": "__addonName__",
"version": "__buildVersion__",
"description": "__description__",
"homepage_url": "__homepage__",
"author": "__author__",
"icons": {
"48": "chrome/content/icons/icon@0.5x.png",
"96": "chrome/content/icons/icon.png"
},
"applications": {
"zotero": {
"id": "__addonID__",
"update_url": "__updateURL__",
"strict_min_version": "7.999",
"strict_max_version": "8.*.*"
}
}
}
================================================
FILE: addon/prefs.js
================================================
/* eslint-disable no-undef */
pref("firstRun", true);
pref("translatorsMended", false);
/* tools */
pref("autoSplitName", false);
pref("splitEnName", false);
pref("language", "zh");
/* retrieve metadata */
pref("autoUpdateMetadata", true);
pref("namePattern", "{%t}_{%g}");
pref("namePatternCustom", "{%t}");
pref("metadataSource", "CNKI");
pref("isMainlandChina", true);
pref("cnkiAttachmentCookie", "");
pref("similarityThresholdForMetaData", "0.6");
/* match pdf */
pref("pdfMatchFolder", "");
pref("actionAfterAttachmentImport", "backup")
pref("similarityThreshold", "0.8");
pref("topMatchCount", 3);
/* update translators */
pref("autoUpdateTranslators", true);
pref("translatorUpdateTime", "0");
pref("translatorSource", "");
/* bookmark */
pref("enableBookmark", true);
pref("newNodeAsChild", false);
pref("disableZoteroOutline", true);
================================================
FILE: doc/README-zhCN.md
================================================
# Zotero Plugin Template
[](https://www.zotero.org)
[](https://github.com/windingwind/zotero-plugin-template)
这是 [Zotero](https://www.zotero.org/) 的插件模板.
[English](../README.md) | [简体中文](./README-zhCN.md)
- 开发指南
- [📖 插件开发文档](https://zotero-chinese.com/plugin-dev-guide/) (中文版,尚不完善)
- [📖 Zotero 7 插件开发文档](https://www.zotero.org/support/dev/zotero_7_for_developers)
- 开发工具参考
- [🛠️ Zotero 插件工具包](https://github.com/windingwind/zotero-plugin-toolkit) | [API 文档](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md)
- [🛠️ Zotero 插件开发脚手架](https://github.com/northword/zotero-plugin-scaffold)
- [📜 Zotero 源代码](https://github.com/zotero/zotero)
- [ℹ️ Zotero 类型定义](https://github.com/windingwind/zotero-types)
- [📌 Zotero 插件模板](https://github.com/windingwind/zotero-plugin-template) (即本仓库)
> [!tip]
> 👁 Watch 本仓库,以及时收到修复或更新的通知.
## 使用此模板构建的插件
[](https://github.com/windingwind/zotero-better-notes)
[](https://github.com/windingwind/zotero-pdf-preview)
[](https://github.com/windingwind/zotero-pdf-translate)
[](https://github.com/windingwind/zotero-tag)
[](https://github.com/iShareStuff/ZoteroTheme)
[](https://github.com/MuiseDestiny/zotero-reference)
[](https://github.com/MuiseDestiny/zotero-citation)
[](https://github.com/MuiseDestiny/ZoteroStyle)
[](https://github.com/volatile-static/Chartero)
[](https://github.com/l0o0/tara)
[](https://github.com/redleafnew/delitemwithatt)
[](https://github.com/redleafnew/zotero-updateifsE)
[](https://github.com/northword/zotero-format-metadata)
[](https://github.com/inciteful-xyz/inciteful-zotero-plugin)
[](https://github.com/MuiseDestiny/zotero-gpt)
[](https://github.com/zoushucai/zotero-journalabbr)
[](https://github.com/MuiseDestiny/zotero-figure)
[](https://github.com/l0o0/jasminum)
[](https://github.com/lifan0127/ai-research-assistant)
[](https://github.com/daeh/zotero-markdb-connect)
如果你正在使用此库,我建议你将这个标志 ([](https://github.com/windingwind/zotero-plugin-template)) 放在 README 文件中:
```md
[](https://github.com/windingwind/zotero-plugin-template)
```
## Features 特性
- 事件驱动、函数式编程的可扩展框架;
- 简单易用,开箱即用;
- ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#自动热重载)
- `src/modules/examples.ts` 中有丰富的示例,涵盖了插件中常用的大部分API (使用 [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit);
- TypeScript 支持:
- 为使用 JavaScript 编写的 Zotero 源码提供全面的类型定义支持 (使用 [zotero-types](https://github.com/windingwind/zotero-types));
- 全局变量和环境设置;
- 插件开发/构建/发布工作流:
- 自动生成/更新插件版本、更新配置和设置环境变量 (`development`/`production`);
- 自动在 Zotero 中构建和重新加载代码;
- 自动发布到 GitHub ;
- 集成 Prettier 和 ES Lint;
## Examples 示例
此库提供了 [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit) 中API的示例.
在 `src/examples.ts` 中搜索`@example` 查看示例. 这些示例在 `src/hooks.ts` 中调用演示.
### 基本示例(Basic Examples)
- registerNotifier
- registerPrefs, unregisterPrefs
### 快捷键示例(Shortcut Keys Examples)
- registerShortcuts
- exampleShortcutLargerCallback
- exampleShortcutSmallerCallback
- exampleShortcutConflictionCallback
### UI示例(UI Examples)

- registerStyleSheet(the official make-it-red example)
- registerRightClickMenuItem
- registerRightClickMenuPopup
- registerWindowMenuWithSeprator
- registerExtraColumn
- registerExtraColumnWithCustomCell
- registerCustomItemBoxRow
- registerLibraryTabPanel
- registerReaderTabPanel
### 首选项面板示例(Preference Pane Examples)

- Preferences bindings
- UI Events
- Table
- Locale
详情参见 [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
### 帮助示例(HelperExamples)

- dialogExample
- clipboardExample
- filePickerExample
- progressWindowExample
- vtableExample(See Preference Pane Examples)
### 指令行示例(PromptExamples)
Obsidian风格的指令输入模块,它通过接受文本来运行插件,并在弹出窗口中显示可选项.
使用 `Shift+P` 激活.

- registerAlertPromptExample
## 快速上手
### 0 环境要求
1. 安装 [beta 版 Zotero](https://www.zotero.org/support/beta_builds)
2. 安装 [Node.js](https://nodejs.org/en/) 和 [Git](https://git-scm.com/)
> [!note]
> 本指南假定你已经对 Zotero 插件的基本结构和工作原理有初步的了解. 如果你还不了解,请先参考[官方文档](https://www.zotero.org/support/dev/zotero_7_for_developers) 和[官方插件样例 Make It Red](https://github.com/zotero/make-it-red)。
### 1 创建你的仓库(Create Your Repo)
1. 点击 `Use this template`;
2. 使用 `git clone` 克隆上一步生成的仓库;
<details >
<summary>💡 从 GitHub Codespace 开始</summary>
_GitHub CodeSpace_ 使你可以直接开始开发而无需在本地下载代码/IDE/依赖.
重复下列步骤,仅需三十秒即可开始构建你的第一个插件!
- 点击首页 `Use this template` 按钮,随后点击 `Open in codespace`, 你需要登录你的 GitHub 账号.
- 等待 codespace 加载.
</details>
3. 进入项目文件夹;
### 2 配置模板和开发环境(Config Template Settings and Enviroment)
1. 修改 `./package.json` 中的设置,包括:
```json5
{
version: "", // 修改为 0.0.0
author: "",
description: "",
homepage: "",
config: {
addonName: "", // 插件名称
addonID: "", // 插件 ID 【重要:防止冲突】
addonRef: "", // 插件命名空间:元素前缀等
addonInstance: "", // 注册在 Zotero 根下的实例名
prefsPrefix: "extensions.zotero.${addonRef}", // 首选项的前缀
},
}
```
> [!warning]
> 注意设置 addonID 和 addonRef 以避免冲突.
如果你需要在 GitHub 以外的地方托管你的 XPI 包,请修改 `zotero-plugin.config.ts` 中的 `updateURL` 和 `xpiDownloadLink`。
2. 复制 Zotero 启动配置,填入 Zotero 可执行文件路径和 profile 路径.
> (可选项) 创建开发用 profile 目录:
>
> 此操作仅需执行一次: 使用 `/path/to/zotero -p` 启动 Zotero,创建一个新的配置文件并用作开发配置文件。
```sh
cp .env.example .env
vim .env
```
如果你维护了多个插件,可以将这些内容存入系统环境变量,以避免在每个插件中都需要重复设置。
3. 运行 `npm install` 以安装相关依赖
> 如果你使用 `pnpm` 作为包管理器,你需要添加 `public-hoist-pattern[]=*@types/bluebird*` 到`.npmrc`, 详情请查看 [zotero-types](https://github.com/windingwind/zotero-types?tab=readme-ov-file#usage) 的文档.
如果你使用 `npm install` 的过程中遇到了 `npm ERR! ERESOLVE unable to resolve dependency tree` ,这是由于上游依赖 typescript-eslint 导致的错误,请使用 `npm i -f` 命令进行安装。
### 3 开发插件
使用 `npm start` 启动开发服务器,它将:
- 在开发模式下预构建插件
- 启动 Zotero ,并让其从 `build/` 中加载插件
- 打开开发者工具(devtool)
- 监听 `src/**` 和 `addon/**`.
- 如果 `src/**` 修改了,运行 esbuild 并且重新加载
- 如果 `addon/**` 修改了,(在开发模式下)重新构建插件并且重新加载
#### 自动热重载
厌倦了无休止的重启吗?忘掉它,拥抱热加载!
1. 运行 `npm start`.
2. 编码. (是的,就这么简单)
当检测到 `src` 或 `addon` 中的文件修改时,插件将自动编译并重新加载.
<details style="text-indent: 2em">
<summary>💡 将此功能添加到现有插件的步骤</summary>
请参阅:[zotero-plugin-scaffold](https://github.com/northword/zotero-plugin-scaffold)。
</details>
#### 调试代码
你还可以:
- 在 Tools->Developer->Run Javascript 中测试代码片段;
- 使用 `Zotero.debug()` 调试输出. 在 Help->Debug Output Logging->View Output 查看输出;
- 调试 UI. Zotero 建立在 Firefox XUL 框架之上. 使用 [XUL Explorer](https://udn.realityripple.com/docs/Archive/Mozilla/XUL_Explorer) 等软件调试 XUL UI.
> XUL 文档: <http://www.devdoc.net/web/developer.mozilla.org/en-US/docs/XUL.html>
### 4 构建插件
运行 `npm run build` 在生产模式下构建插件,构建的结果位于 `build/` 目录中.
构建步骤:
- 创建/清空 `build/`
- 复制 `addon/**` 到 `build/addon/**`
- 替换占位符:使用 `replace-in-file` 去替换在 `package.json` 中定义的关键字和配置 (`xhtml`、`.flt` 等)
- 准备本地化文件以避免冲突,查看官方文档了解更多(<https://www.zotero.org/support/dev/zotero_7_for_developers#avoiding_localization_conflicts)>
- 重命名`**/*.flt` 为 `**/${addonRef}-*.flt`
- 在每个消息前加上 `addonRef-`
- 使用 Esbuild 来将 `.ts` 源码构建为 `.js`,从 `src/index.ts` 构建到`./build/addon/chrome/content/scripts`
- (仅在生产模式下工作) 压缩 `./build/addon` 目录为 `./build/*.xpi`
- (仅在生产模式下工作) 准备 `update.json` 或 `update-beta.json`
> [!note]
>
> **Dev & prod 两者有什么区别?**
>
> - 此环境变量存储在 `Zotero.${addonInstance}.data.env` 中,控制台输出在生产模式下被禁用.
> - 你可以根据此变量决定用户无法查看/使用的内容.
> - 在生产模式下,构建脚本将自动打包插件并更新 `update.json`.
### 5 发布
如果要构建和发布插件,运行如下指令:
```shell
# version increase, git add, commit and push
# then on ci, npm run build, and release to GitHub
npm run release
```
> [!note]
> 在此模板中,release-it 被配置为在本地更新版本号、提交并推送标签,随后 GitHub Action 将重新构建插件并将 XPI 发布到 GitHub Release.
#### 关于预发布
该模板将 `prerelease` 定义为插件的测试版,当你在 release-it 中选择 `prerelease` 版本 (版本号中带有 `-` ),构建脚本将创建一个 `update-beta.json` 给预发布版本使用,这将确保常规版本的用户不会自动更新到测试版,只有手动下载并安装了测试版的用户才能自动更新到下一个测试版. 当下一个正式版本更新时,脚本将同步更新 `update.json` 和 `update-beta.json`,这将使正式版和测试版用户都可以更新到最新的正式版.
> [!warning]
> 严格来说,区分 Zotero 6 和 Zotero 7 兼容的插件版本应该通过 `update.json` 的 `addons.__addonID__.updates[]` 中分别配置 `applications.zotero.strict_min_version`,这样 Zotero 才能正确识别,详情在 Zotero 7 开发文档(<https://www.zotero.org/support/dev/zotero_7_for_developers#updaterdf_updatesjson)获取>.
## Details 更多细节
### 关于Hooks(About Hooks)
> 可以在 [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/hooks.ts) 中查看更多
1. 当在 Zotero 中触发安装/启用/启动时,`bootstrap.js` > `startup` 被调用
- 等待 Zotero 就绪
- 加载 `index.js` (插件代码的主入口,从 `index.ts` 中构建)
- 如果是 Zotero 7 以上的版本则注册资源
2. 主入口 `index.js` 中,插件对象被注入到 `Zotero` ,并且 `hooks.ts` > `onStartup` 被调用.
- 初始化插件需要的资源,包括通知监听器、首选项面板和UI元素.
3. 当在 Zotero 中触发卸载/禁用时,`bootstrap.js` > `shutdown` 被调用.
- `events.ts` > `onShutdown` 被调用. 移除 UI 元素、首选项面板或插件创建的任何内容.
- 移除脚本并释放资源.
### 关于全局变量(About Global Variables)
> 可以在 [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/index.ts)中查看更多
bootstrap插件在沙盒中运行,但沙盒中没有默认的全局变量,例如 `Zotero` 或 `window` 等我们曾在overlay插件环境中使用的变量.
此模板将以下变量注册到全局范围:
```ts
Zotero, ZoteroPane, Zotero_Tabs, window, document, rootURI, ztoolkit, addon;
```
### 创建元素 API(Create Elements API)
插件模板为 bootstrap 插件提供了一些新的API. 我们有两个原因使用这些 API,而不是使用 `createElement/createElementNS`:
- 在 bootstrap 模式下,插件必须在推出(禁用或卸载)时清理所有 UI 元素,这非常麻烦. 使用 `createElement`,插件模板将维护这些元素. 仅仅在退出时 `unregisterAll` .
- Zotero 7 需要 createElement()/createElementNS() → createXULElement() 来表示其他的 XUL 元素,而 Zotero 6 并不支持 `createXULElement`. 类似于 React.createElement 的API `createElement` 检测 namespace(xul/html/svg) 并且自动创建元素,返回元素为对应的 TypeScript 元素类型.
```ts
createElement(document, "div"); // returns HTMLDivElement
createElement(document, "hbox"); // returns XUL.Box
createElement(document, "button", { namespace: "xul" }); // manually set namespace. returns XUL.Button
```
### 关于 Zotero API(About Zotero API)
Zotero 文档已过时且不完整,克隆 <https://github.com/zotero/zotero> 并全局搜索关键字.
> ⭐[zotero-types](https://github.com/windingwind/zotero-types) 提供了最常用的 Zotero API,在默认情况下它被包含在此模板中. 你的 IDE 将为大多数的 API 提供提醒.
猜你需要:查找所需 API的技巧
在 `.xhtml`/`.flt` 文件中搜索 UI 标签,然后在 locale 文件中找到对应的键. ,然后在 `.js`/`.jsx` 文件中搜索此键.
### 目录结构(Directory Structure)
本部分展示了模板的目录结构.
- 所有的 `.js/.ts` 代码都在 `./src`;
- 插件配置文件:`./addon/manifest.json`;
- UI 文件: `./addon/chrome/content/*.xhtml`.
- 区域设置文件: `./addon/locale/**/*.flt`;
- 首选项文件: `./addon/prefs.js`;
> 不要在 `prefs.js` 中换行
```shell
.
|-- .eslintrc.json # eslint conf
|-- .gitattributes # git conf
|-- .github/ # github conf
|-- .gitignore # git conf
|-- .prettierrc # prettier conf
|-- .release-it.json # release-it conf
|-- .vscode # vs code conf
| |-- extensions.json
| |-- launch.json
| |-- setting.json
| `-- toolkit.code-snippets
|-- package-lock.json # npm conf
|-- package.json # npm conf
|-- LICENSE
|-- README.md
|-- addon
| |-- bootstrap.js # addon load/unload script, like a main.c
| |-- chrome
| | `-- content
| | |-- icons/
| | |-- preferences.xhtml # preference panel
| | `-- zoteroPane.css
| |-- locale # locale
| | |-- en-US
| | | |-- addon.ftl
| | | `-- preferences.ftl
| | `-- zh-CN
| | |-- addon.ftl
| | `-- preferences.ftl
| |-- manifest.json # addon config
| `-- prefs.js
|-- build/ # build dir
|-- scripts # scripts for dev
| |-- build.mjs # script to build plugin
| |-- scripts.mjs # scripts send to Zotero, such as reload, openDevTool, etc
| |-- server.mjs # script to start a development server
| |-- start.mjs # script to start Zotero process
| |-- stop.mjs # script to kill Zotero process
| |-- utils.mjs # utils functions for dev scripts
| |-- update-template.json # template of `update.json`
| `-- zotero-cmd-template.json # template of local env
|-- src # source code
| |-- addon.ts # base class
| |-- hooks.ts # lifecycle hooks
| |-- index.ts # main entry
| |-- modules # sub modules
| | |-- examples.ts
| | `-- preferenceScript.ts
| `-- utils # utilities
| |-- locale.ts
| |-- prefs.ts
| |-- wait.ts
| `-- window.ts
|-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig
|-- typings # ts typings
| `-- global.d.ts
`-- update.json
```
## Disclaimer 免责声明
在 AGPL 下使用此代码. 不提供任何保证. 遵守你所在地区的法律!
如果你想更改许可,请通过 <wyzlshx@foxmail.com> 与我联系.
================================================
FILE: eslint.config.mjs
================================================
// @ts-check Let TS check this config file
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
{
ignores: ["build/**", "dist/**", "node_modules/**", "scripts/"],
},
{
extends: [eslint.configs.recommended, ...tseslint.configs.recommended],
rules: {
"no-restricted-globals": [
"error",
{ message: "Use `Zotero.getMainWindow()` instead.", name: "window" },
{
message: "Use `Zotero.getMainWindow().document` instead.",
name: "document",
},
{
message: "Use `Zotero.getActiveZoteroPane()` instead.",
name: "ZoteroPane",
},
"Zotero_Tabs",
],
"@typescript-eslint/ban-ts-comment": [
"warn",
{
"ts-expect-error": "allow-with-description",
"ts-ignore": "allow-with-description",
"ts-nocheck": "allow-with-description",
"ts-check": "allow-with-description",
},
],
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": [
"off",
{
ignoreRestArgs: true,
},
],
"@typescript-eslint/no-non-null-assertion": "off",
},
},
);
================================================
FILE: package.json
================================================
{
"name": "jasminum",
"version": "1.1.31",
"description": "一个简单的 Zotero 中文插件",
"config": {
"addonName": "Jasminum",
"addonID": "jasminum@linxzh.com",
"addonRef": "jasminum",
"addonInstance": "Jasminum",
"prefsPrefix": "extensions.jasminum"
},
"repository": {
"type": "git",
"url": "git+https://github.com/l0o0/jasminum.git"
},
"author": "l0o0",
"bugs": {
"url": "https://github.com/l0o0/jasminum/issues"
},
"homepage": "https://github.com/l0o0/jasminum#readme",
"license": "AGPL-3.0-or-later",
"scripts": {
"start": "zotero-plugin serve",
"build": "tsc --noEmit && zotero-plugin build",
"lint": "prettier --write . && eslint . --fix",
"release": "zotero-plugin release",
"test": "echo \"Error: no test specified\" && exit 1",
"update-deps": "npm update --save"
},
"dependencies": {
"pdf-lib": "^1.17.1",
"string-similarity": "^4.0.4",
"zotero-plugin-toolkit": "5.1.0-beta.4"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@types/node": "^25.0.10",
"@types/string-similarity": "^4.0.2",
"eslint": "^9.27.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.33.0",
"zotero-plugin-scaffold": "^0.8.0",
"zotero-types": "4.1.0-beta.4"
},
"prettier": {
"printWidth": 80,
"tabWidth": 2,
"endOfLine": "lf",
"overrides": [
{
"files": [
"*.xhtml"
],
"options": {
"htmlWhitespaceSensitivity": "css"
}
}
]
},
"packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf"
}
================================================
FILE: src/addon.ts
================================================
import hooks from "./hooks";
import { createZToolkit } from "./utils/ztoolkit";
import { Progress } from "./modules/progress";
import { VirtualizedTableHelper } from "zotero-plugin-toolkit";
import { MyCookieSandbox } from "./utils/cookiebox";
import { getOutlineFromPDF } from "./modules/outline/outline";
import { TaskRunner } from "./utils/task";
import { requestDocument } from "./utils/http";
class Addon {
public data: {
alive: boolean;
// Env type, see build.js
env: "development" | "production";
ztoolkit: ZToolkit;
locale?: {
current: any;
};
prefs?: {
window: Window;
};
progress: Progress;
windows: Record<string, Window>;
translators: {
window?: Window;
helper?: VirtualizedTableHelper;
rows: TableRow[];
allRows: TableRow[];
selected?: string;
updating?: boolean;
};
myCookieSandbox: MyCookieSandbox;
isImportingAttachments: boolean;
};
// Lifecycle hooks
public hooks: typeof hooks;
// APIs
public api: object;
public taskRunner: TaskRunner;
constructor() {
this.data = {
alive: true,
env: __env__,
ztoolkit: createZToolkit(),
progress: new Progress(),
windows: {},
translators: {
rows: [],
allRows: [],
updating: false,
},
myCookieSandbox: new MyCookieSandbox(),
isImportingAttachments: false,
};
this.hooks = hooks;
this.api = { getOutlineFromPDF, requestDocument };
this.taskRunner = new TaskRunner();
}
}
export default Addon;
================================================
FILE: src/hooks.ts
================================================
import { config } from "../package.json";
import { initLocale } from "./utils/locale";
import {
registerPrefsPane,
onPrefsWindowLoad,
initPrefs,
} from "./modules/preferences/main";
import { createZToolkit } from "./utils/ztoolkit";
import { registerMenu } from "./modules/menu";
import {
registerExtraColumnWithCustomCell,
registerNotifiers,
registerTab,
} from "./modules/notifier";
import { injectStylesLink } from "./modules/styles";
import { updateTranslators } from "./modules/translators";
import { getPref } from "./utils/prefs";
async function onStartup() {
await Promise.all([
Zotero.initializationPromise,
Zotero.unlockPromise,
Zotero.uiReadyPromise,
]);
initLocale();
registerPrefsPane();
initPrefs();
registerNotifiers();
registerMenu();
registerTab();
await registerExtraColumnWithCustomCell();
injectStylesLink();
// @ts-ignore - Not typed.
await Zotero.Promise.delay(1000);
await Promise.all(
Zotero.getMainWindows().map((win) => onMainWindowLoad(win)),
);
}
async function onMainWindowLoad(win: Window): Promise<void> {
// Create ztoolkit for every window
addon.data.ztoolkit = createZToolkit();
// @ts-ignore - Not typed.
await Zotero.Promise.delay(1000);
if (getPref("autoUpdateTranslators")) {
// @ts-ignore - Not typed.
await Zotero.Promise.delay(10000);
ztoolkit.log("auto update translators");
updateTranslators();
}
}
function onShutdown(): void {
ztoolkit.unregisterAll();
// Remove addon object
addon.data.alive = false;
// @ts-ignore - Plugin instance is not typed
delete Zotero[config.addonInstance];
}
// Add your hooks here. For element click, etc.
// Keep in mind hooks only do dispatch. Don't add code that does real jobs in hooks.
// Otherwise the code would be hard to read and maintain.
export default {
onStartup,
onShutdown,
onMainWindowLoad,
onPrefsWindowLoad,
};
================================================
FILE: src/index.ts
================================================
import { BasicTool } from "zotero-plugin-toolkit";
import Addon from "./addon";
import { config } from "../package.json";
const basicTool = new BasicTool();
// @ts-ignore - Plugins instance not typed.
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
_globalThis.addon = new Addon();
defineGlobal("ztoolkit", () => {
return _globalThis.addon.data.ztoolkit;
});
// @ts-ignore - Plugins instance not typed.
Zotero[config.addonInstance] = addon;
}
function defineGlobal(name: Parameters<BasicTool["getGlobal"]>[0]): void;
function defineGlobal(name: string, getter: () => any): void;
function defineGlobal(name: string, getter?: () => any) {
Object.defineProperty(_globalThis, name, {
get() {
return getter ? getter() : basicTool.getGlobal(name);
},
});
}
================================================
FILE: src/modules/attachments/index.ts
================================================
import { getPref } from "../../utils/prefs";
import { LocalAttachmentService } from "./localMatch";
const localService = new LocalAttachmentService();
export async function attachmentSearch(task: AttachmentTask): Promise<void> {
const attachmentSearchResults = await localService.searchAttachments(task);
if (!attachmentSearchResults || attachmentSearchResults.length === 0) {
task.addMsg("No matching attachments found in local.");
task.status = "fail";
return;
} else if (attachmentSearchResults.length === 1) {
task.searchResults = attachmentSearchResults;
task.resultIndex = 0;
task.addMsg("Found one matching attachment in local.");
} else {
task.status = "multiple_results";
task.searchResults = attachmentSearchResults;
task.addMsg(
`Found ${attachmentSearchResults.length} matching attachments in local.`,
);
}
}
export async function importAttachment(task: AttachmentTask): Promise<void> {
// Maybe oneday I will support remote attachment import
await localService.importAttachment(task);
// Action after import
await actionAfterImport(task.searchResults![task.resultIndex!].url);
}
export async function actionAfterImport(
attachmentPath: string,
action?: string,
): Promise<void> {
const a = action || getPref("actionAfterAttachmentImport") || "nothing";
const attachmentName = PathUtils.filename(attachmentPath);
const backupFolder = PathUtils.join(
getPref("pdfMatchFolder"),
"jasminum-backup",
);
const backupFile = PathUtils.join(backupFolder, attachmentName);
ztoolkit.log("Action after import: ", a, attachmentName);
switch (a) {
case "nothing":
ztoolkit.log("No action after import.");
break;
case "backup":
ztoolkit.log("Backing up the attachment...");
await IOUtils.makeDirectory(backupFolder, { ignoreExisting: true });
await IOUtils.move(attachmentPath, backupFile);
break;
case "delete":
ztoolkit.log("Deleting the attachment...");
await IOUtils.remove(attachmentPath);
break;
}
}
================================================
FILE: src/modules/attachments/localMatch.ts
================================================
import { compareTwoStrings } from "string-similarity";
import { getPref } from "../../utils/prefs";
import { isChineseAttachmentFilename } from "../../utils/detect";
// Return full path of the attachments.
export async function findAttachmentsInFolder(
folder?: string,
): Promise<string[]> {
if (!folder) folder = getPref("pdfMatchFolder");
ztoolkit.log(folder);
return (await IOUtils.getChildren(folder)).filter((filename) => {
ztoolkit.log(filename);
return isChineseAttachmentFilename(PathUtils.filename(filename));
});
}
export class LocalAttachmentService implements AttachmentService {
async searchAttachments(
task: AttachmentTask,
): Promise<AttachmentSearchResult[] | null> {
ztoolkit.log("Searching for local attachments...");
const threshold = parseFloat(getPref("similarityThreshold"));
const top = getPref("topMatchCount");
const searchString = task.item.getField("title");
const attachmentFilenames = await findAttachmentsInFolder();
ztoolkit.log(attachmentFilenames);
if (!attachmentFilenames || attachmentFilenames.length === 0) {
return null;
}
// 创建包含评分和文件名的对象数组
const scoredItems = attachmentFilenames.map((filename) => {
const name = PathUtils.filename(filename);
const name_no_ext = name.replace(/\.(pdf|caj|kdh|nh)$/i, "");
const score = compareTwoStrings(
searchString.toUpperCase(),
name_no_ext.toUpperCase(),
);
ztoolkit.log(
searchString.toUpperCase(),
name,
name_no_ext.toUpperCase(),
score,
);
return {
title: name,
filename: name,
score: score,
url: filename,
source: "local",
};
});
ztoolkit.log(scoredItems);
// 按评分降序排序
const sortedItems = scoredItems.sort((a, b) => b.score - a.score);
// 过滤阈值并取前3项
const topMatches = sortedItems
.filter((item) => item.score >= threshold)
.slice(0, top);
return topMatches.length > 0 ? topMatches : null;
}
async importAttachment(task: AttachmentTask): Promise<void> {
if (
!task.searchResults ||
task.searchResults.length === 0 ||
task.resultIndex === undefined
) {
task.addMsg("Found attachment, but import failed.");
task.status = "fail";
return;
}
const searchResult = task.searchResults[task.resultIndex];
const importOptions: _ZoteroTypes.Attachments.OptionsFromFile = {
file: searchResult.url,
parentItemID: task.item.id,
title: `FullText_by_Jasminum.${searchResult.title}`,
};
const importItem = await Zotero.Attachments.importFromFile(importOptions);
if (importItem) {
task.status = "success";
}
}
}
================================================
FILE: src/modules/menu.ts
================================================
import { MenuitemOptions } from "zotero-plugin-toolkit/dist/managers/menu";
import { config } from "../../package.json";
import { getString } from "../utils/locale";
import {
mergeName,
splitName,
updateCNKICite,
importAttachmentsFromFolder,
handleAttachmentMenu,
} from "./tools";
import { isChineseTopAttachment, isChinsesSnapshot } from "../utils/detect";
const metaddataMenuItems: MenuitemOptions[] = [
{
tag: "menuitem",
label: "retrieveMetadata",
icon: `chrome://${config.addonRef}/content/icons/searchCNKI.png`,
isHidden: (_elm, _ev) =>
Zotero.getActiveZoteroPane()
.getSelectedItems()
.some((item) => {
return !(isChineseTopAttachment(item) || isChinsesSnapshot(item));
}),
commandListener: async () => {
const items = Zotero.getActiveZoteroPane().getSelectedItems();
for (const item of items) {
await addon.taskRunner.createAndAddTask(
item,
isChineseTopAttachment(item) ? "attachment" : "snapshot",
);
}
},
},
{
tag: "menuitem",
label: "retrieveMetadataForBook",
icon: `chrome://${config.addonRef}/content/icons/searchCNKI.png`,
isHidden: () => true,
// getVisibility: (_elm, _ev) =>
// Zotero.getActiveZoteroPane()
// .getSelectedItems()
// .some((item) => {
// return isChineseTopAttachment(item);
// }),
commandListener: () => {
// @ts-ignore - The plugin instance is not typed.
Zotero[config.addonInstance].scraper.search(
Zotero.getActiveZoteroPane().getSelectedItems()[0],
);
},
},
];
const toolsMenuItems: MenuitemOptions[] = [
{
tag: "menuitem",
label: "mergeName",
icon: `chrome://${config.addonRef}/content/icons/name.png`,
commandListener: () => {
for (const item of Zotero.getActiveZoteroPane().getSelectedItems()) {
mergeName(item);
}
},
},
{
tag: "menuitem",
label: "splitName",
icon: `chrome://${config.addonRef}/content/icons/name.png`,
commandListener: () => {
for (const item of Zotero.getActiveZoteroPane().getSelectedItems()) {
splitName(item);
}
},
},
{
tag: "menuitem",
label: "updateCNKICite",
icon: `chrome://${config.addonRef}/content/icons/cite.png`,
commandListener: async () => {
await updateCNKICite(Zotero.getActiveZoteroPane().getSelectedItems());
},
},
{
tag: "menuitem",
label: "find-attachment",
icon: `chrome://${config.addonRef}/content/icons/attachment-search.svg`,
commandListener: () => {
handleAttachmentMenu("item");
},
},
];
export function registerMenu() {
const separatorMenu: MenuitemOptions = {
tag: "menuseparator",
id: `${config.addonRef}-separator`,
isHidden: (_event) =>
Zotero.getActiveZoteroPane()
.getSelectedItems()
.some((item) => {
return !(
isChineseTopAttachment(item) ||
isChinsesSnapshot(item) ||
(item.isTopLevelItem() && item.isRegularItem())
);
}),
};
const metadataMenu: MenuitemOptions = {
tag: "menu",
label: getString("menu-metadata"),
id: `${config.addonRef}-metadata-menu`,
icon: `chrome://${config.addonRef}/content/icons/icon.png`,
children: metaddataMenuItems.map((subOption) => {
const label = subOption.label as string;
subOption.id = `${config.addonRef}-menuitem-${label}`;
subOption.label = getString(`menuitem-${label}`);
return subOption;
}),
isHidden: (_event) =>
Zotero.getActiveZoteroPane()
.getSelectedItems()
.some((item) => {
return !(isChineseTopAttachment(item) || isChinsesSnapshot(item));
}),
};
const toolsMenu: MenuitemOptions = {
tag: "menu",
label: getString("menu-tools"),
id: `${config.addonRef}-tools-menu`,
icon: `chrome://${config.addonRef}/content/icons/icon.png`,
children: toolsMenuItems.map((subOption) => {
const label = subOption.label as string;
subOption.id = `${config.addonRef}-menuitem-${label}`;
subOption.label = getString(`menuitem-${label}`);
return subOption;
}),
isHidden: () =>
Zotero.getActiveZoteroPane()
.getSelectedItems()
.some((item) => {
return !(item.isTopLevelItem() && item.isRegularItem());
}),
};
ztoolkit.Menu.register("item", separatorMenu);
ztoolkit.Menu.register("item", metadataMenu);
ztoolkit.Menu.register("item", toolsMenu);
const attachmentMenu: MenuitemOptions = {
tag: "menuitem",
label: getString("menuitem-find-attachment"),
id: `${config.addonRef}-attachment-menu`,
icon: `chrome://${config.addonRef}/content/icons/attachment-search.svg`,
commandListener: () => {
handleAttachmentMenu("collection");
},
isHidden: () =>
Zotero.getActiveZoteroPane().getSelectedCollection() === undefined
? true
: false,
};
const importAttachmentMenu: MenuitemOptions = {
tag: "menuitem",
label: getString("menuitem-import-attachments"),
id: `${config.addonRef}-attachment-menu`,
icon: `chrome://${config.addonRef}/content/icons/folder-import.svg`,
commandListener: async () => {
await importAttachmentsFromFolder();
},
isHidden: () =>
Zotero.getActiveZoteroPane().getSelectedCollection() === undefined
? true
: false,
};
ztoolkit.Menu.register("collection", attachmentMenu);
ztoolkit.Menu.register("collection", importAttachmentMenu);
// ztoolkit.Menu.register("item", {
// tag: "menuitem",
// label: "TEST",
// commandListener: async () => {
// // downloadTranslator(true);
// const item = Zotero.getActiveZoteroPane().getSelectedItems()[0];
// const title = await getPDFTitle(item.id);
// ztoolkit.log(title);
// },
// });
// Disable in collection
// ztoolkit.Menu.register("collection", metadataMenu);
}
================================================
FILE: src/modules/notifier.ts
================================================
import { config } from "../../package.json";
import { getString } from "../utils/locale";
import { getPref } from "../utils/prefs";
import { isChineseTopAttachment } from "../utils/detect";
import { registerOutline } from "./outline";
import { splitName } from "./tools";
/**
* A wrap for Zotero.Notifier.registerObserver,
* which will automatically unregister the observer when the addon is disabled.
*/
function registerNotifier(
onNotify: (
event: string,
type: string,
ids: number[] | string[],
extraData: { [key: string]: any },
) => void,
types: _ZoteroTypes.Notifier.Type[],
) {
const callback = {
notify: async (
event: string,
type: string,
ids: number[] | string[],
extraData: { [key: string]: any },
) => {
if (!addon?.data.alive) {
unregisterNotifier(notifierID);
return;
}
onNotify(event, type, ids, extraData);
},
};
// Register the callback in Zotero as an item observer
const notifierID = Zotero.Notifier.registerObserver(callback, types);
Zotero.Plugins.addObserver({
shutdown: ({ id }) => {
if (id === config.addonID) unregisterNotifier(notifierID);
},
});
}
function unregisterNotifier(notifierID: string) {
Zotero.Notifier.unregisterObserver(notifierID);
}
/**
* Register notifiers for the addon at startup hooks.
*/
export function registerNotifiers() {
registerNotifier(onAddItem, ["item"]);
// registerNotifier(onOpenTab, ["tab"]);
}
async function onAddItem(
event: string,
type: string,
ids: Array<string | number>,
extraData: { [key: string]: any },
) {
// ztoolkit.log(`notify: add item, event: ${event}, type: ${type}, ids: ${ids}`);
if (event !== "add" || type !== "item") return;
for (const id of ids) {
const item = Zotero.Items.get(id);
if (getPref("autoUpdateMetadata")) {
if (isChineseTopAttachment(item)) {
await addon.taskRunner.createAndAddTask(item, "attachment");
}
}
if (getPref("autoSplitName")) {
splitName(item);
}
}
}
// TODO: Complete the notifier.
// async function onOpenTab(
// event: string,
// type: string,
// ids: Array<string | number>,
// extraData: { [key: string]: any },
// ) {
// const id = ids[0];
// if (
// (event == "select" || event == "load") &&
// type == "tab" &&
// extraData[id].type == "reader"
// ) {
// ztoolkit.log("onOpenTab", event, type, extraData);
// if (getPref("enableBookmark")) {
// await registerOutline(id as string);
// } else {
// ztoolkit.log("Jasminum bookmark is disabled");
// }
// }
// }
export async function registerExtraColumnWithCustomCell() {
const registeredDataKey = Zotero.ItemTreeManager.registerColumn({
dataKey: "CNKIcitation",
label: getString("CNKIcitation"),
pluginID: config.addonID,
dataProvider: (item, dataKey) => {
// 网友提供的特殊字符,方便排序
return ztoolkit.ExtraField.getExtraField(item, "CNKICite") || "\u2068";
},
// @ts-ignore - Not typed.
// renderCell(index, data, column, isFirstColumn, doc) {
// const span = doc.createElementNS("http://www.w3.org/1999/xhtml", "span");
// span.className = `cell ${column.className}`;
// span.title = getString("CNKIcitation");
// span.innerText = data == "" ? null : data;
// return span;
// },
});
}
// For Outline register.
export function registerTab() {
Zotero.Reader.registerEventListener(
"renderToolbar",
tabRegisterCallback,
config.addonID,
);
// Zotero.Reader.registerEventListener(
// "renderTextSelectionPopup",
// (event: any) => {
// ztoolkit.log(event);
// event.append("<div>Jasminum</div>");
// },
// );
}
async function tabRegisterCallback(event: any) {
if (getPref("enableBookmark")) {
const { reader } = event;
await registerOutline(reader.tabID);
} else {
ztoolkit.log("Jasminum bookmark is disabled");
}
}
================================================
FILE: src/modules/outline/bookmark.ts
================================================
import { version } from "../../../package.json";
import { getString } from "../../utils/locale";
import { ICONS } from "./style";
import { OUTLINE_SCHEMA } from "./outline";
export const BOOKMARK_SCHEMA = OUTLINE_SCHEMA;
export const DEFAULT_BOOKMARK_FONT_SIZE = 13; // Default font size for bookmarks
// 学生友好的清新现代颜色
export const DEFAULT_BOOKMARK_COLORS = [
"#FF6B6B", // 珊瑚红
"#4ECDC4", // 薄荷绿
"#45B7D1", // 天空蓝
"#96CEB4", // 薄荷色
"#FECA57", // 向日葵黄
"#FF9FF3", // 粉紫色
"#54A0FF", // 宝蓝色
"#5F27CD", // 紫罗兰
"#00D2D3", // 青绿色
"#FF9F43", // 橙色
"#10AC84", // 翡翠绿
"#EE5A24", // 朱砂橙
];
// 获取随机颜色
function getRandomBookmarkColor(): string {
const randomIndex = Math.floor(
Math.random() * DEFAULT_BOOKMARK_COLORS.length,
);
return DEFAULT_BOOKMARK_COLORS[randomIndex];
}
function migrateBookmarkInfo(
raw: any,
fromSchema: number,
): { bookmarks: BookmarkNode[]; baseFontSize: number } {
let bookmarks: BookmarkNode[] = raw.bookmarks ?? [];
let baseFontSize = DEFAULT_BOOKMARK_FONT_SIZE;
// v1 → v2: add baseFontSize and bookmark color
if (fromSchema < 2) {
baseFontSize = raw.info?.baseFontSize ?? DEFAULT_BOOKMARK_FONT_SIZE;
bookmarks = bookmarks.map((b: any) => ({
...b,
color: b.color || getRandomBookmarkColor(),
}));
}
// Future v2 → v3 migrations go here
return { bookmarks, baseFontSize };
}
function getReaderPagePosition(): PdfPosition {
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
const primaryView = reader._internalReader
._primaryView as _ZoteroTypes.Reader.PDFView;
const PDFViewerApplication = primaryView._iframeWindow!.PDFViewerApplication;
const doc = primaryView._iframeWindow!.document;
const container = doc.getElementById("viewerContainer")!;
const pageIndex = PDFViewerApplication.pdfViewer!.currentPageNumber - 1;
const pageView = PDFViewerApplication.pdfViewer!.getPageView(pageIndex);
const viewport = pageView.viewport;
const scrollX = 0;
const scrollY = container.scrollTop - pageView.div.offsetTop;
const [x, y] = viewport.convertToPdfPoint(scrollX, scrollY);
return { position: { pageIndex, rects: [[x, y, x, y]] } };
}
export async function saveBookmarksToJSON(
item?: Zotero.Item,
bookmarks?: BookmarkNode[],
baseFontSize?: number,
) {
if (!bookmarks) {
bookmarks = getBookmarksFromPage();
}
if (!item) {
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
item = reader._item;
}
// Get current baseFontSize if not provided
if (baseFontSize === undefined) {
const currentInfo = await loadBookmarkInfoFromJSON(item);
baseFontSize = currentInfo?.baseFontSize ?? DEFAULT_BOOKMARK_FONT_SIZE;
}
const bookmarkInfo: BookmarkInfo = {
info: {
itemID: item.id,
schema: BOOKMARK_SCHEMA,
jasminumVersion: version,
baseFontSize: baseFontSize,
},
bookmarks: bookmarks,
};
const bookmarkStr = JSON.stringify(bookmarkInfo);
const bookmarkPath = PathUtils.join(
Zotero.DataDirectory.dir,
"storage",
item.key,
"jasminum-bookmarks.json",
);
await Zotero.File.putContentsAsync(bookmarkPath, bookmarkStr);
ztoolkit.log("Save bookmarks to JSON");
}
export async function loadBookmarkInfoFromJSON(
item: Zotero.Item,
): Promise<{ bookmarks: BookmarkNode[]; baseFontSize: number } | null> {
const bookmarkPath = PathUtils.join(
Zotero.DataDirectory.dir,
"storage",
item.key,
"jasminum-bookmarks.json",
);
const isFileExist = await IOUtils.exists(bookmarkPath);
if (!isFileExist) {
ztoolkit.log(`Bookmarks json is missing: ${bookmarkPath}`);
return null;
} else {
const content = (await Zotero.File.getContentsAsync(
bookmarkPath,
)) as string;
const tmp = JSON.parse(content);
const fileSchema = tmp.info?.schema ?? 1;
if (fileSchema < BOOKMARK_SCHEMA) {
// Migrate old bookmark data instead of discarding
const migrated = migrateBookmarkInfo(tmp, fileSchema);
await saveBookmarksToJSON(
item,
migrated.bookmarks,
migrated.baseFontSize,
);
return migrated;
} else {
const bookmarkInfo = JSON.parse(content) as BookmarkInfo;
return {
bookmarks: bookmarkInfo.bookmarks,
baseFontSize:
bookmarkInfo.info.baseFontSize ?? DEFAULT_BOOKMARK_FONT_SIZE,
};
}
}
}
export async function loadBookmarksFromJSON(
item: Zotero.Item,
): Promise<BookmarkNode[] | null> {
const info = await loadBookmarkInfoFromJSON(item);
return info?.bookmarks ?? null;
}
export function getBookmarksFromPage(): BookmarkNode[] {
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
const rootUL = reader._iframeWindow!.document.querySelector(
"#bookmark-root-list",
);
if (!rootUL) return [];
const bookmarkItems = Array.from(rootUL.querySelectorAll("li.bookmark-item"));
if (bookmarkItems.length === 0) {
ztoolkit.log("No bookmarks found on this page.");
return [];
}
return bookmarkItems.map((li, index) => {
const bookmarkDiv = (li as Element).querySelector("div.bookmark-node")!;
const titleSpan = (li as Element).querySelector("span.bookmark-title")!;
return {
id: bookmarkDiv.getAttribute("data-id")!,
title: titleSpan.textContent!,
page: parseInt(bookmarkDiv.getAttribute("page")!),
x: parseFloat(bookmarkDiv.getAttribute("x")!),
y: parseFloat(bookmarkDiv.getAttribute("y")!),
order: index,
createdAt: parseInt(bookmarkDiv.getAttribute("data-created") || "0"),
color:
bookmarkDiv.getAttribute("data-color") || DEFAULT_BOOKMARK_COLORS[0],
};
});
}
export function createBookmarkNodes(
nodes: BookmarkNode[] | null,
parentElement: HTMLElement,
doc: Document,
) {
if (nodes === null || nodes.length == 0) {
ztoolkit.UI.appendElement(
{
tag: "div",
namespace: "html",
classList: ["empty-bookmark-prompt"],
properties: { innerHTML: `请点击上方按钮${ICONS.add}创建书签` },
},
parentElement,
);
} else {
// 按order排序
const sortedNodes = [...nodes].sort((a, b) => a.order - b.order);
sortedNodes.forEach((node) => {
const li = ztoolkit.UI.createElement(doc, "li", {
namespace: "html",
classList: ["bookmark-item"],
children: [
{
tag: "div",
namespace: "html",
classList: ["bookmark-node"],
attributes: {
draggable: "true",
"data-id": node.id,
page: node.page,
x: node.x,
y: node.y,
"data-created": node.createdAt,
"data-color": node.color,
},
styles: {
borderLeftColor: node.color,
},
children: [
{
tag: "div",
namespace: "html",
classList: ["bookmark-content"],
children: [
{
tag: "span",
namespace: "html",
classList: ["bookmark-title"],
properties: { textContent: node.title },
attributes: {
title: `${node.title}, Page: ${node.page}`,
},
},
],
},
],
},
],
});
parentElement.appendChild(li);
});
}
}
// 生成智能书签名称
function generateSmartBookmarkTitle(pageNumber: number): string {
const existingBookmarks = getBookmarksFromPage();
const baseName = `P_${pageNumber}_`;
// 检查是否有重名
const existingTitles = existingBookmarks.map((b) => b.title);
// 找到下一个可用的数字后缀
let counter = 1;
let candidateName = `${baseName}${counter}`;
while (existingTitles.includes(candidateName)) {
counter++;
candidateName = `${baseName}${counter}`;
}
return candidateName;
}
export function addNewBookmark(title?: string): BookmarkNode {
const location = getReaderPagePosition();
const now = Date.now();
const pageNumber = location.position.pageIndex + 1;
return {
id: `bookmark_${now}_${Math.random().toString(36).substr(2, 9)}`,
title: title || generateSmartBookmarkTitle(pageNumber),
page: pageNumber,
x: location.position.rects[0][0],
y: location.position.rects[0][1],
order: now, // 使用时间戳作为默认排序
createdAt: now,
color: getRandomBookmarkColor(),
};
}
export function addBookmarkButton(doc: Document) {
if (doc.querySelector("#sidebarContainer div.start") === null) {
ztoolkit.log("Sidebar toolbar button is missing.");
}
ztoolkit.UI.appendElement(
{
tag: "button",
namespace: "html",
id: "j-bookmark-button",
classList: ["toolbar-button"],
properties: { innerHTML: ICONS.bookmark },
attributes: {
title: getString("bookmark"),
tabindex: "-1",
role: "tab",
"aria-selected": "false",
"aria-controls": "j-bookmark-viewer",
},
},
doc.querySelector("#sidebarContainer div.start")!,
);
}
// Update bookmark font size dynamically
export function updateBookmarkFontSize(doc: Document, baseFontSize: number) {
const styleId = "jasminum-bookmark-dynamic-font-size";
let styleElement = doc.getElementById(styleId) as HTMLStyleElement;
if (!styleElement) {
styleElement = doc.createElement("style");
styleElement.id = styleId;
styleElement.type = "text/css";
doc.querySelector("head")!.appendChild(styleElement);
}
const dynamicCSS = `
.bookmark-node {
font-size: ${baseFontSize}px !important;
}
`;
styleElement.textContent = dynamicCSS;
ztoolkit.log(`Updated bookmark font size: ${baseFontSize}px`);
}
================================================
FILE: src/modules/outline/events.ts
================================================
import {
saveOutlineToJSON,
createTreeNodes,
getOutlineFromPDF,
updateOutlineFontSize,
loadOutlineInfoFromJSON,
DEFAULT_BASE_FONT_SIZE,
} from "./outline";
import {
saveBookmarksToJSON,
createBookmarkNodes,
addNewBookmark,
DEFAULT_BOOKMARK_COLORS,
updateBookmarkFontSize,
loadBookmarkInfoFromJSON,
DEFAULT_BOOKMARK_FONT_SIZE,
} from "./bookmark";
import { ICONS } from "./style";
import { getString } from "../../utils/locale";
import { getPref } from "../../utils/prefs";
const MAX_LEVEL = 7;
function getReaderPagePosition(): PdfPosition {
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
const primaryView = reader._internalReader
._primaryView as _ZoteroTypes.Reader.PDFView;
const PDFViewerApplication = primaryView._iframeWindow!.PDFViewerApplication;
const doc = primaryView._iframeWindow!.document;
const container = doc.getElementById("viewerContainer")!;
const pageIndex = PDFViewerApplication.pdfViewer!.currentPageNumber - 1;
const pageView = PDFViewerApplication.pdfViewer!.getPageView(pageIndex);
const viewport = pageView.viewport;
// const scrollX = container.scrollLeft - pageView.div.offsetLeft;
const scrollX = 0;
const scrollY = container.scrollTop - pageView.div.offsetTop;
const [x, y] = viewport.convertToPdfPoint(scrollX, scrollY);
ztoolkit.log(
"get position",
pageIndex + 1,
container.scrollTop,
scrollX,
scrollY,
x,
y,
);
return { position: { pageIndex, rects: [[x, y, x, y]] } };
}
export function initEventListener(
reader: _ZoteroTypes.ReaderInstance,
doc: Document,
) {
// Hide or show side bar
function hideShowMyOutlineAndBar(e: Event) {
const targetElement = e.target as Element;
const button = targetElement.closest("button");
if (!button) return;
ztoolkit.log("click to hide outline/bookmark", targetElement, button);
// Enable j outline view
if (button.id === "j-outline-button") {
ztoolkit.log("jasminum show outline");
reader.setSidebarView("jasminum-outline");
doc.getElementById("jasminum-outline")?.classList.remove("hidden");
doc.getElementById("jasminum-bookmarks")?.classList.add("hidden");
doc
.getElementById("j-outline-toolbar")
?.classList.toggle("j-hidden", false);
doc
.getElementById("j-bookmark-toolbar")
?.classList.toggle("j-hidden", true);
button.classList.toggle("active", true);
doc
.getElementById("j-bookmark-button")
?.classList.toggle("active", false);
} else if (button.id === "j-bookmark-button") {
ztoolkit.log("jasminum show bookmark");
reader.setSidebarView("jasminum-bookmarks");
doc.getElementById("jasminum-bookmarks")?.classList.remove("hidden");
doc.getElementById("jasminum-outline")?.classList.add("hidden");
doc
.getElementById("j-bookmark-toolbar")
?.classList.toggle("j-hidden", false);
doc
.getElementById("j-outline-toolbar")
?.classList.toggle("j-hidden", true);
button.classList.toggle("active", true);
doc.getElementById("j-outline-button")?.classList.toggle("active", false);
} else {
// Hide both outline and bookmark views
ztoolkit.log("hide jasminum views");
doc.getElementById("jasminum-outline")?.classList.toggle("hidden", true);
doc
.getElementById("jasminum-bookmarks")
?.classList.toggle("hidden", true);
doc
.getElementById("j-outline-toolbar")
?.classList.toggle("j-hidden", true);
doc
.getElementById("j-bookmark-toolbar")
?.classList.toggle("j-hidden", true);
doc.getElementById("j-outline-button")?.classList.toggle("active", false);
doc
.getElementById("j-bookmark-button")
?.classList.toggle("active", false);
}
}
// 给默认按钮添加事件,避免切换面板时异常
doc
.querySelector("#sidebarContainer > div.sidebar-toolbar > div.start")
?.addEventListener("click", hideShowMyOutlineAndBar);
const treeContainer = doc.getElementById("j-outline-viewer");
if (!treeContainer) return;
// 节点展开/折叠事件,选中节点
treeContainer // 节点点击选择事件
.addEventListener("click", async (e: Event) => {
const target = e.target as HTMLElement;
ztoolkit.log("click container", e.target);
// 检查是否点击的是展开/折叠图标
const spanElement = target.closest("span");
if (spanElement && spanElement.classList.contains("expander")) {
ztoolkit.log("click expander");
const listItem = target.closest("li");
if (!listItem) return;
toggleNode(listItem);
e.stopPropagation();
await saveOutlineToJSON();
return;
}
// 节点选择
if (target.closest(".tree-node")) {
selectNode(target.closest(".tree-node")!);
clickToPosition(target);
}
});
// 双击编辑节点
treeContainer.addEventListener("dblclick", function (e) {
if ((e.target as Element).classList.contains("node-title")) {
makeNodeEditable(e.target as Element);
e.stopPropagation();
}
});
// 书签上方工具栏事件
doc
.getElementById("j-outline-expand-all")
?.addEventListener("click", expandAll);
doc
.getElementById("j-outline-collapse-all")
?.addEventListener("click", collapseAll);
doc
.getElementById("j-outline-add-node")
?.addEventListener("click", addNewNode);
doc
.getElementById("j-outline-delete-node")
?.addEventListener("click", deleteSelectedNode);
doc
.getElementById("j-outline-save-pdf")
?.addEventListener("click", async (ev: Event) => {
const button = ev.currentTarget as HTMLButtonElement;
button.disabled = true;
await addOutlineToPDFRunner();
button.disabled = false;
});
// 拖拽相关事件
treeContainer.addEventListener("dragstart", handleDragStart);
treeContainer.addEventListener("dragover", handleDragOver);
treeContainer.addEventListener("dragleave", handleDragLeave);
treeContainer.addEventListener("drop", handleDrop);
treeContainer.addEventListener("dragend", handleDragEnd);
// 处理键盘事件
treeContainer.addEventListener("keydown", handleKeydownEvent);
// 点击书签跳转到具体页码
// 书签相关事件处理
const bookmarkContainer = doc.getElementById("j-bookmark-viewer");
if (bookmarkContainer) {
// 书签点击选择和跳转事件
bookmarkContainer.addEventListener("click", async (e: Event) => {
const target = e.target as HTMLElement;
ztoolkit.log("click bookmark container", e.target);
// 书签选择和跳转
if (target.closest(".bookmark-node")) {
selectBookmarkNode(target.closest(".bookmark-node")!);
clickToBookmarkPosition(target);
}
});
// 双击编辑书签
bookmarkContainer.addEventListener("dblclick", function (e) {
if ((e.target as Element).classList.contains("bookmark-title")) {
makeBookmarkNodeEditable(e.target as Element);
e.stopPropagation();
}
});
// 书签拖拽相关事件
bookmarkContainer.addEventListener("dragstart", handleBookmarkDragStart);
bookmarkContainer.addEventListener("dragover", handleBookmarkDragOver);
bookmarkContainer.addEventListener("dragleave", handleBookmarkDragLeave);
bookmarkContainer.addEventListener("drop", handleBookmarkDrop);
bookmarkContainer.addEventListener("dragend", handleBookmarkDragEnd);
}
// 书签工具栏事件
doc
.getElementById("j-bookmark-add")
?.addEventListener("click", addNewBookmarkNode);
doc
.getElementById("j-bookmark-delete")
?.addEventListener("click", deleteSelectedBookmarkNode);
// 字体大小调整按钮事件
doc
.getElementById("j-outline-zoom-in")
?.addEventListener("click", handleFontSizeIncrease);
doc
.getElementById("j-outline-zoom-out")
?.addEventListener("click", handleFontSizeDecrease);
}
// 为节点添加事件监听,以下为事件处理函数
async function expandAll(ev: Event) {
const doc = (ev.target as Element).ownerDocument;
const collapsedNodes = doc.querySelectorAll(".tree-item.collapsed");
collapsedNodes.forEach((node) => {
node.classList.remove("collapsed");
const expander = node.querySelector(".expander");
if (expander?.hasChildNodes()) {
//expander!.textContent = "▼";
expander!.innerHTML = ICONS.down;
}
});
await saveOutlineToJSON();
}
async function collapseAll(ev: Event) {
const doc = (ev.target as Element).ownerDocument;
const parentNodes = doc.querySelectorAll(".tree-item.has-children");
parentNodes.forEach((node) => {
node.classList.add("collapsed");
const expander = node.querySelector(".expander");
if (expander?.hasChildNodes()) {
//expander!.textContent = "►";
expander!.innerHTML = ICONS.right;
}
});
await saveOutlineToJSON();
}
// 切换节点展开/折叠状态
function toggleNode(node: Element) {
if (node.classList.contains("has-children")) {
node.classList.toggle("collapsed");
// 更新展开/折叠图标
const expander = node.querySelector(".expander");
if (node.classList.contains("collapsed")) {
//expander!.textContent = "►";
expander!.innerHTML = ICONS.right;
} else {
//expander!.textContent = "▼";
expander!.innerHTML = ICONS.down;
}
}
}
// 选择节点
function selectNode(node: Element) {
const doc = node.ownerDocument;
const selectedNode = doc.querySelector(".node-selected");
// 取消之前的选择
if (selectedNode) {
selectedNode.classList.remove("node-selected");
}
// 设置新选择
node.classList.add("node-selected");
}
// Key events for the outline panel.
export async function handleKeydownEvent(ev: KeyboardEvent) {
const newPanel = (ev.target! as Element).ownerDocument.getElementById(
"root-list",
)!;
const nodes = Array.from(newPanel.querySelectorAll("div.tree-node"));
const selectedNode = newPanel.querySelector("div.tree-node.node-selected");
let currentIdx = nodes.indexOf(selectedNode as Element);
// ztoolkit.log("Keydown event", currentIdx, ev);
if (ev.key === "ArrowDown") {
while (currentIdx < nodes.length - 1) {
const nextNode = nodes[currentIdx + 1] as HTMLElement;
// ztoolkit.log("Next node", currentIdx, nextNode);
if (nextNode && nextNode.checkVisibility()) {
nextNode.querySelector<HTMLElement>("span.node-title")!.click();
nextNode.focus();
break;
}
currentIdx += 1;
}
}
if (ev.key === "ArrowUp") {
while (currentIdx > 0) {
const nextNode = nodes[currentIdx - 1] as HTMLElement;
if (nextNode && nextNode.checkVisibility()) {
nextNode.querySelector<HTMLElement>("span.node-title")!.click();
nextNode.focus();
break;
}
currentIdx -= 1;
}
}
if (ev.key === "ArrowLeft" || ev.key === "ArrowRight") {
(selectedNode?.querySelector("span.expander") as HTMLElement).click();
}
if (ev.key === " ") {
// ztoolkit.log("Space key pressed", selectedNode);
ev.preventDefault();
makeNodeEditable(
selectedNode!.querySelector<HTMLElement>("span.node-title")!,
);
}
if (ev.key === "Delete" || ev.key === "Backspace") {
// ztoolkit.log("Delete key pressed");
deleteSelectedNode(ev);
}
// Level up
if (ev.key === "[") {
// ztoolkit.log("[ key pressed");
const targetNode = (ev.target as Element).querySelector<Element>(
".node-selected",
)!;
const targetLi = targetNode.closest("li")!;
const oldParentUl = targetLi.parentElement!;
const oldGrandParent = oldParentUl.parentElement!;
// 如果是根节点,直接返回
if (oldParentUl.id === "root-list") return;
oldParentUl.removeChild(targetLi);
// 此时原来的父节点已经没有子节点了,删除
if (oldParentUl.children.length === 0) {
oldGrandParent.removeChild(oldParentUl);
oldGrandParent.classList.remove("has-children");
const expander = oldGrandParent.querySelector(".expander")!;
expander.textContent = " ";
}
oldGrandParent.parentElement!.insertBefore(
targetLi,
oldGrandParent.nextSibling,
);
updateNodeLevels(targetLi);
await saveOutlineToJSON();
}
// Level down
if (ev.key === "]") {
// ztoolkit.log("] key pressed");
const targetNode = (ev.target as Element).querySelector<Element>(
".node-selected",
)!;
const targetLi = targetNode.closest("li")!;
const parentLi = targetLi.previousElementSibling;
if (!parentLi) return;
let parentUl = parentLi.querySelector("ul");
// 如果没有子列表,创建一个
if (!parentUl) {
parentUl = targetNode.ownerDocument.createElement("ul");
parentUl.classList.add("tree-list");
parentLi.appendChild(parentUl);
// 更新父节点状态
parentLi.classList.add("has-children");
const expander = parentLi.querySelector(".expander")!;
// expander.textContent = "▼";
expander.innerHTML = ICONS.down;
}
// 添加到子列表
parentUl.appendChild(targetLi);
// 确保目标节点展开
targetLi.classList.remove("collapsed");
updateNodeLevels(targetLi);
await saveOutlineToJSON();
}
// Add new node
if (ev.key === "\\") {
// ztoolkit.log("\\ key pressed");
addNewNode(ev);
}
}
export function handleDragStart(e: DragEvent) {
// if (!(e.target instanceof HTMLElement)) return;
ztoolkit.log(" start to drag");
const target = e.target as Element;
if (!target.classList.contains("tree-node")) return;
const draggedNode = target.closest("li") as HTMLElement;
e.dataTransfer!.setData("text/plain", draggedNode.innerText);
e.dataTransfer!.effectAllowed = "move";
// 为拖拽中的元素添加样式
setTimeout(() => {
draggedNode.classList.add("dragging");
}, 0);
}
// 拖拽经过目标元素
export function handleDragOver(e: DragEvent) {
e.preventDefault();
e.dataTransfer!.dropEffect = "move";
const target = e.target as HTMLElement;
const doc = target.ownerDocument;
// 修复坐标异常
const upperHeight =
doc.querySelector("html")?.getBoundingClientRect().height || 41;
const draggedNode = doc.querySelector(".dragging");
if (!draggedNode) return;
// if (!(e.target instanceof HTMLElement)) return;
// 找到最近的节点元素
const targetNode = target.closest(".tree-node");
if (!targetNode) {
hideDropIndicator(doc);
return;
}
// 不能拖拽到自己或自己的子元素
const targetLi = targetNode.closest("li") as Element;
if (draggedNode === targetLi || isAncestor(draggedNode, targetLi)) {
hideDropIndicator(doc);
return;
}
// 计算拖拽位置(上方、中间放入其中、下方)
const rect = targetNode.getBoundingClientRect();
const mouseY = e.clientY;
const relativeY = mouseY - rect.top;
const height = rect.height;
let dropPosition;
if (relativeY < height * 0.25) {
dropPosition = "before";
} else if (relativeY > height * 0.75) {
dropPosition = "after";
} else {
dropPosition = "inside";
}
// 如果位置或目标变化了,才更新指示器
// 临时数据暂时存储在window中
if (
doc.defaultView!.lastDropPosition !== dropPosition ||
doc.defaultView!.lastDropTarget !== targetLi
) {
updateDropIndicator(targetNode, dropPosition, upperHeight);
doc.defaultView!.lastDropPosition = dropPosition;
doc.defaultView!.lastDropTarget = targetLi;
}
// 添加可放置样式
doc.querySelectorAll(".dragover").forEach((el) => {
el.classList.remove("dragover");
});
targetNode.classList.add("dragover");
}
function updateDropIndicator(
targetNode: Element,
position: string,
upperHeight: number,
) {
const rect = targetNode.getBoundingClientRect();
const doc = targetNode.ownerDocument;
const dropIndicator = doc.querySelector(".drop-indicator") as HTMLElement;
// 清除所有位置类
dropIndicator.classList.remove("top", "middle", "bottom");
dropIndicator.classList.add("visible");
if (position === "before") {
dropIndicator.classList.add("top");
dropIndicator.style.left = `${rect.left}px`;
dropIndicator.style.top = `${rect.top - 2 - upperHeight}px`;
dropIndicator.style.width = `${rect.width}px`;
} else if (position === "after") {
dropIndicator.classList.add("bottom");
dropIndicator.style.left = `${rect.left}px`;
dropIndicator.style.top = `${rect.bottom - upperHeight}px`;
dropIndicator.style.width = `${rect.width}px`;
} else {
// inside position
dropIndicator.classList.add("middle");
dropIndicator.style.left = `${rect.left + 20}px`;
dropIndicator.style.top = `${rect.top + rect.height / 2 - upperHeight}px`;
dropIndicator.style.width = `${rect.width - 25}px`;
}
}
function hideDropIndicator(doc: Document) {
const dropIndicator = doc.querySelector(".drop-indicator")!;
dropIndicator.classList.remove("visible");
doc.defaultView!.lastDropPosition = null;
doc.defaultView!.lastDropTarget = null;
}
// 拖拽离开目标元素
export function handleDragLeave(e: DragEvent) {
const doc = (e.target as Element).ownerDocument;
if (
!e.relatedTarget ||
!(e.relatedTarget as Element).closest("#j-outline-viewer")
) {
hideDropIndicator(doc);
}
const targetNode = (e.target as HTMLElement).closest(".tree-node");
if (targetNode) {
// 移除可放置样式
targetNode.classList.remove("dragover");
}
}
// 处理放置
export async function handleDrop(e: DragEvent) {
e.preventDefault();
// if (!(e.target instanceof HTMLElement)) return;
const target = e.target as HTMLElement;
const doc = target.ownerDocument;
const draggedNode = doc.querySelector(".dragging");
// 隐藏指示器
hideDropIndicator(doc);
if (!draggedNode) return;
// 获取目标节点
const targetTreeNode = target.closest(".tree-node");
if (!targetTreeNode) return;
// 移除可放置样式
doc.querySelectorAll(".dragover").forEach((el) => {
el.classList.remove("dragover");
});
// 获取目标列表项
const targetLi = targetTreeNode.closest("li")!;
// 不能将节点拖到自己或其子节点上
if (draggedNode === targetLi || isAncestor(draggedNode, targetLi)) {
return;
}
// 移除拖拽的节点
const oldParent = draggedNode.parentNode! as HTMLElement;
oldParent.removeChild(draggedNode);
// 判断放置位置:是作为子节点还是兄弟节点
const dropPosition = determineDropPosition(e, targetTreeNode);
if (dropPosition === "child") {
// 作为子节点
let targetUl = targetLi.querySelector("ul");
// 如果没有子列表,创建一个
if (!targetUl) {
targetUl = doc.createElement("ul");
targetUl.classList.add("tree-list");
targetLi.appendChild(targetUl);
// 更新父节点状态
targetLi.classList.add("has-children");
const expander = targetLi.querySelector(".expander")!;
// expander.textContent = "▼";
expander.innerHTML = ICONS.down;
}
// 确保目标节点展开
targetLi.classList.remove("collapsed");
// 添加到子列表
targetUl.appendChild(draggedNode);
} else {
// 作为兄弟节点
const targetParent = targetLi.parentNode!;
if (dropPosition === "before") {
targetParent.insertBefore(draggedNode, targetLi);
} else {
// 'after'
targetParent.insertBefore(draggedNode, targetLi.nextSibling);
}
}
// 如果原父列表为空,更新其父节点状态
if (
oldParent.children.length === 0 &&
oldParent.tagName === "UL" &&
oldParent !== doc.getElementById("root-list")
) {
const oldGrandParent = oldParent.parentNode as HTMLElement;
oldGrandParent.removeChild(oldParent);
oldGrandParent.classList.remove("has-children");
const expander = oldGrandParent.querySelector(".expander")!;
expander.textContent = " ";
}
// 更新节点级别样式
updateNodeLevels(draggedNode);
// 保存节点信息
await saveOutlineToJSON();
}
// 拖拽结束
export function handleDragEnd(e: DragEvent) {
// if (!(e.target instanceof HTMLElement)) return;
const doc = (e.target as HTMLElement).ownerDocument;
const draggedNode = doc.querySelector(".dragging");
if (!draggedNode) return;
draggedNode.classList.remove("dragging");
// 隐藏指示器
hideDropIndicator(doc);
// 清除所有dragover样式
doc.querySelectorAll(".dragover").forEach((el) => {
el.classList.remove("dragover");
});
}
// 检查一个节点是否是另一个节点的祖先
function isAncestor(ancestor: Element, descendant: Element) {
let current = descendant.parentNode;
while (current) {
if (current === ancestor) {
return true;
}
current = current.parentNode;
}
return false;
}
// 确定放置位置:作为子节点、同级前面或同级后面
function determineDropPosition(event: DragEvent, targetNode: Element) {
const rect = targetNode.getBoundingClientRect();
const mouseY = event.clientY;
// 上三分之一区域放在前面,下三分之一区域放在后面,中间放在内部
const relativeY = mouseY - rect.top;
const height = rect.height;
if (relativeY < height / 3) {
return "before";
} else if (relativeY > (height * 2) / 3) {
return "after";
} else {
return "child";
}
}
// 更新节点及其子节点的级别样式
function updateNodeLevels(node: Element) {
const updateLevel = (element: Element, level: number) => {
const nodeDiv = element.querySelector(".tree-node")!;
// 移除所有级别类
for (let i = 1; i <= MAX_LEVEL; i++) {
nodeDiv.classList.remove(`level-${i}`);
}
// 添加正确的级别类
nodeDiv.classList.add(`level-${level}`);
nodeDiv.setAttribute("level", level.toString());
// 递归处理子节点
const childList = element.querySelector("ul");
if (childList) {
Array.from(childList.children).forEach((child) => {
updateLevel(child, level + 1);
});
}
};
// 计算当前节点的级别
let level = 1;
let parent = node.parentNode as Element;
while (parent && parent.id !== "root-list") {
if (parent.tagName === "UL") {
level++;
}
parent = parent.parentNode as Element;
}
updateLevel(node, level);
}
export function makeNodeEditable(titleElement: Element) {
const doc = titleElement.ownerDocument;
const parent = titleElement.parentNode! as Element;
const treeNode = titleElement.closest("div.tree-node")!;
// 获取当前值
const currentTitle = titleElement.textContent || "";
const currentPage = treeNode.getAttribute("page")!;
// 创建容器
const container = doc.createElement("div");
container.style.display = "flex";
container.style.gap = "5px";
// 创建标题输入框
const titleInput = doc.createElement("input");
titleInput.type = "text";
titleInput.value = currentTitle.trim();
titleInput.placeholder = getString("outline-edit-placeholder");
// 替换原始元素
container.appendChild(titleInput);
// container.appendChild(pageInput);
parent.replaceChild(container, titleElement);
// 聚焦到标题输入框
titleInput.focus();
// 禁用拖拽功能
treeNode.setAttribute("draggable", "false");
// 保存逻辑
const saveChanges = async () => {
const newTitle = titleInput.value.trim();
// 更新原始元素
titleElement.textContent = newTitle || currentTitle;
titleElement.setAttribute("title", `${newTitle}, Page: ${currentPage}`);
treeNode.setAttribute("page", currentPage);
// 恢复 DOM 结构
parent.replaceChild(titleElement, container);
// 恢复拖拽功能
treeNode.setAttribute("draggable", "true");
// 保存节点信息
await saveOutlineToJSON();
};
// 事件处理
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
saveChanges();
doc.getElementById("j-outline-viewer")!.focus();
} else if (e.key === "Escape") {
parent.replaceChild(titleElement, container);
}
e.stopPropagation();
// 保留焦点
};
const handleBlur = (e: FocusEvent) => {
if (!container.contains(e.relatedTarget as Node)) {
saveChanges();
}
};
// 绑定事件
titleInput.addEventListener("keydown", handleKeyDown);
container.addEventListener("blur", handleBlur, true);
}
// 删除选中节点
export async function deleteSelectedNode(ev: Event) {
const doc = (ev.target as Element).ownerDocument;
const selectedNode = doc.querySelector<HTMLElement>(".node-selected")!;
const rootNode = doc.getElementById("root-list");
if (!selectedNode || !rootNode) return;
const listItem = selectedNode.closest("li")!;
const beforeSelectedLi = listItem.previousElementSibling;
const parent = listItem.parentNode as HTMLElement;
// 如果有子节点,则进行提示确认是否删除
if (listItem.classList.contains("has-children")) {
const confirmDelete = ztoolkit.getGlobal("confirm")(
getString("outline-delete-confirm"),
);
if (!confirmDelete) return;
}
// 移除节点
parent.removeChild(listItem);
// 如果父列表没有其他子元素,更新其父节点的状态
if (
parent.children.length === 0 &&
parent.tagName === "UL" &&
parent !== doc.getElementById("root-list")
) {
const parentLi = parent.parentNode as HTMLElement;
parentLi.removeChild(parent);
parentLi.classList.remove("has-children");
const expander = parentLi.querySelector(".expander")!;
expander.textContent = " ";
}
// 保存节点信息
await saveOutlineToJSON();
if (!rootNode.hasChildNodes()) {
ztoolkit.UI.appendElement(
{
tag: "div",
namespace: "html",
classList: ["empty-outline-prompt"],
properties: {
innerHTML: getString("outline-empty-prompt", {
args: { icon: ICONS.add },
}),
},
},
rootNode,
);
}
if (beforeSelectedLi) {
beforeSelectedLi
.querySelector("div.tree-node")
?.classList.add("node-selected");
} else {
parent.parentNode
?.querySelector("div.tree-node")
?.classList.add("node-selected");
}
doc.getElementById("j-outline-viewer")?.focus();
}
// 添加新节点。选中节点的子节点还是下一个同级节点
// 默认设置为添加节点的同级节点
export async function addNewNode(ev: Event) {
const doc = (ev.target as Element).ownerDocument;
const newTitle = "新书签";
const selectedNode = doc.querySelector(".node-selected");
const location = getReaderPagePosition();
// 如果没有选中节点,添加到根
if (!selectedNode) {
const rootList = doc.getElementById("root-list")!;
createTreeNodes(
[
{
level: 1,
title: newTitle,
page: location.position.pageIndex + 1,
x: location.position.rects[0][0],
y: location.position.rects[0][1],
},
],
rootList,
doc,
);
doc.querySelector(".empty-outline-prompt")?.classList.add("hidden");
} else {
// 添加为选中节点的子节点或兄弟节点
let targetChildrenList: HTMLElement;
let targetLevel: number;
const selectedLevel = parseInt(selectedNode.getAttribute("level") || "1");
if (getPref("newNodeAsChild")) {
// 作为子节点
const selectedLi = selectedNode.closest("li.tree-item")!;
targetLevel = selectedLevel + 1;
// 检查是否有子列表,如果没有,创建一个
targetChildrenList = selectedLi.querySelector("ul")!;
if (!targetChildrenList) {
targetChildrenList = ztoolkit.UI.createElement(doc, "ul", {
classList: ["tree-list"],
});
selectedLi.appendChild(targetChildrenList);
// 添加父节点标记并更新展开图标
selectedLi.classList.add("has-children");
const expander = selectedLi.querySelector(".expander")!;
//expander.textContent = "▼";
expander.innerHTML = ICONS.down;
}
// 确保父节点展开
selectedLi.classList.remove("collapsed");
} else {
targetLevel = selectedLevel;
targetChildrenList = selectedNode.closest("ul.tree-list") as HTMLElement;
}
createTreeNodes(
[
{
level: targetLevel,
title: newTitle,
page: location.position.pageIndex + 1,
x: location.position.rects[0][0],
y: location.position.rects[0][1],
},
],
targetChildrenList,
doc,
);
}
// 保存节点信息
await saveOutlineToJSON();
}
function clickToPosition(targetElement: Element) {
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
const treeNode = targetElement.closest("div.tree-node");
if (!treeNode) return;
const page = parseInt(treeNode.getAttribute("page")!);
const x = parseInt(treeNode.getAttribute("x")!);
const y = parseInt(treeNode.getAttribute("y")!);
ztoolkit.log("Click to position", page, x, y);
// const location = {
// position: { pageIndex: page - 1, rects: [[x, y, x, y]] },
// };
// @ts-ignore - not typed
// reader.navigate(location);
const PDFViewerApplication = (
reader._internalReader._primaryView as _ZoteroTypes.Reader.PDFView
)._iframeWindow.PDFViewerApplication;
const pageView = PDFViewerApplication.pdfViewer!.getPageView(page - 1);
// @ts-ignore - Not typed
const [scrollX, scrollY] = pageView.viewport.convertToViewportPoint(x, y);
(
reader._internalReader._primaryView as _ZoteroTypes.Reader.PDFView
)._iframeWindow!.PDFViewerApplication.page = page;
const container = (
reader._internalReader._primaryView as _ZoteroTypes.Reader.PDFView
)._iframeWindow!.document.getElementById("viewerContainer")!;
ztoolkit.log(`Scroll to ${scrollX}, ${scrollY}`);
container.scrollBy(scrollX, scrollY);
}
// Use worker to add outline to PDF
export async function addOutlineToPDFRunner(): Promise<void> {
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
if (!reader) {
ztoolkit.log("No reader found");
return;
}
const outlineNodes = await getOutlineFromPDF(reader);
if (!outlineNodes) {
ztoolkit.log("No outline nodes found");
return;
}
const filePath = reader._item.getFilePath();
const worker = new Worker(
"chrome://jasminum/content/scripts/jasminum-worker.js",
);
worker.onmessage = (event) => {
// @ts-ignore - event.data is not typed
const data = event.data;
ztoolkit.log("data", data);
if (data && data.action === "addOutlineReturn") {
ztoolkit.log("Add outline to PDF return", data);
}
};
return new Promise((resolve, reject) => {
ztoolkit.log(filePath, outlineNodes);
const jobID = Zotero.Utilities.randomString();
// 消息处理器
const handler = (event: MessageEvent<any>) => {
const data = event.data;
ztoolkit.log("Main handler", data);
// 仅处理匹配 jobID 和 action 的消息
if (data?.action !== "addOutlineReturn" || data?.jobID !== jobID) return;
worker.removeEventListener("message", handler as EventListener);
if (data.status === "success") {
resolve(data);
} else {
reject(new Error(data.error || "Unknown error"));
}
};
worker.addEventListener("message", handler as EventListener);
worker.postMessage({ action: "addOutline", jobID, filePath, outlineNodes });
});
}
// ========== 书签相关函数 ==========
// 选择书签节点
function selectBookmarkNode(node: Element) {
const doc = node.ownerDocument;
const selectedNode = doc.querySelector(".bookmark-selected");
// 取消之前的选择
if (selectedNode) {
selectedNode.classList.remove("bookmark-selected");
}
// 设置新选择
node.classList.add("bookmark-selected");
}
// 点击书签跳转到对应位置
function clickToBookmarkPosition(targetElement: Element) {
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
const bookmarkNode = targetElement.closest("div.bookmark-node");
if (!bookmarkNode) return;
const page = parseInt(bookmarkNode.getAttribute("page")!);
const x = parseInt(bookmarkNode.getAttribute("x")!);
const y = parseInt(bookmarkNode.getAttribute("y")!);
ztoolkit.log("Click to bookmark position", page, x, y);
const PDFViewerApplication = (
reader._internalReader._primaryView as _ZoteroTypes.Reader.PDFView
)._iframeWindow!.PDFViewerApplication;
const pageView = PDFViewerApplication.pdfViewer!.getPageView(page - 1);
// @ts-ignore - Not typed
const [scrollX, scrollY] = pageView.viewport.convertToViewportPoint(x, y);
(
reader._internalReader._primaryView as _ZoteroTypes.Reader.PDFView
)._iframeWindow!.PDFViewerApplication.page = page;
const container = (
reader._internalReader._primaryView as _ZoteroTypes.Reader.PDFView
)._iframeWindow!.document.getElementById("viewerContainer")!;
ztoolkit.log(`Scroll to bookmark ${scrollX}, ${scrollY}`);
container.scrollBy(scrollX, scrollY);
}
// 编辑书签节点
export function makeBookmarkNodeEditable(titleElement: Element) {
const doc = titleElement.ownerDocument;
const parent = titleElement.parentNode! as Element;
const bookmarkNode = titleElement.closest("div.bookmark-node")!;
// 获取当前值
const currentTitle = titleElement.textContent || "";
const currentPage = bookmarkNode.getAttribute("page")!;
const currentColor =
bookmarkNode.getAttribute("data-color") || DEFAULT_BOOKMARK_COLORS[0];
// 创建编辑容器
const editContainer = doc.createElement("div");
editContainer.className = "bookmark-edit-container";
// 创建标题输入框
const titleInput = doc.createElement("input");
titleInput.type = "text";
titleInput.value = currentTitle.trim();
titleInput.placeholder = "书签标题";
// 创建颜色选择器容器
const colorContainer = doc.createElement("div");
colorContainer.className = "bookmark-color-picker";
let selectedColor = currentColor;
// 创建颜色选项
DEFAULT_BOOKMARK_COLORS.forEach((color) => {
const colorOption = doc.createElement("div");
colorOption.className = "bookmark-color-option";
if (color === currentColor) {
colorOption.classList.add("selected");
}
colorOption.style.backgroundColor = color;
colorOption.addEventListener("click", () => {
// 更新选中状态
colorContainer.querySelectorAll("div").forEach((opt) => {
opt.classList.remove("selected");
});
colorOption.classList.add("selected");
selectedColor = color;
// 实时更新书签的颜色显示
(bookmarkNode as HTMLElement).style.borderLeftColor = color;
bookmarkNode.setAttribute("data-color", color);
});
colorContainer.appendChild(colorOption);
});
// 创建分隔线
const separator = doc.createElement("div");
separator.className = "bookmark-edit-separator";
editContainer.appendChild(titleInput);
editContainer.appendChild(separator);
editContainer.appendChild(colorContainer);
// 替换原始元素
parent.replaceChild(editContainer, titleElement);
// 聚焦到输入框
titleInput.focus();
// 禁用拖拽功能
bookmarkNode.setAttribute("draggable", "false");
// 保存逻辑
const saveChanges = async () => {
const newTitle = titleInput.value.trim();
// 更新原始元素
titleElement.textContent = newTitle || currentTitle;
titleElement.setAttribute("title", `${newTitle}, Page: ${currentPage}`);
// 更新颜色
bookmarkNode.setAttribute("data-color", selectedColor);
(bookmarkNode as HTMLElement).style.borderLeftColor = selectedColor;
// 恢复 DOM 结构
parent.replaceChild(titleElement, editContainer);
// 恢复拖拽功能
bookmarkNode.setAttribute("draggable", "true");
// 保存书签信息
await saveBookmarksToJSON();
};
// 事件处理
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
saveChanges();
doc.getElementById("j-bookmark-viewer")!.focus();
} else if (e.key === "Escape") {
parent.replaceChild(titleElement, editContainer);
bookmarkNode.setAttribute("draggable", "true");
}
e.stopPropagation();
};
const handleBlur = (e: FocusEvent) => {
if (!editContainer.contains(e.relatedTarget as Node)) {
saveChanges();
}
};
// 绑定事件
titleInput.addEventListener("keydown", handleKeyDown);
editContainer.addEventListener("blur", handleBlur, true);
}
// 添加新书签
export async function addNewBookmarkNode(ev: Event) {
const doc = (ev.target as Element).ownerDocument;
const newBookmark = addNewBookmark();
const rootList = doc.getElementById("bookmark-root-list")!;
// 清除空提示
doc.querySelector(".empty-bookmark-prompt")?.remove();
createBookmarkNodes([newBookmark], rootList, doc);
// 保存书签信息
await saveBookmarksToJSON();
}
// 删除选中的书签
export async function deleteSelectedBookmarkNode(ev: Event) {
const doc = (ev.target as Element).ownerDocument;
const selectedNode = doc.querySelector<HTMLElement>(".bookmark-selected")!;
const rootNode = doc.getElementById("bookmark-root-list");
if (!selectedNode || !rootNode) return;
const listItem = selectedNode.closest("li")!;
const parent = listItem.parentNode as HTMLElement;
// 移除节点
parent.removeChild(listItem);
// 保存书签信息
await saveBookmarksToJSON();
// 如果没有书签了,显示提示
if (!rootNode.hasChildNodes()) {
ztoolkit.UI.appendElement(
{
tag: "div",
namespace: "html",
classList: ["empty-bookmark-prompt"],
properties: { innerHTML: `请点击上方按钮${ICONS.add}创建书签` },
},
rootNode,
);
}
doc.getElementById("j-bookmark-viewer")?.focus();
}
// 书签拖拽开始
export function handleBookmarkDragStart(e: DragEvent) {
ztoolkit.log("start to drag bookmark");
const target = e.target as Element;
if (!target.classList.contains("bookmark-node")) return;
const draggedNode = target.closest("li") as HTMLElement;
e.dataTransfer!.setData("text/plain", draggedNode.innerText);
e.dataTransfer!.effectAllowed = "move";
// 为拖拽中的元素添加样式
setTimeout(() => {
draggedNode.classList.add("dragging");
}, 0);
}
// 书签拖拽经过目标元素
export function handleBookmarkDragOver(e: DragEvent) {
e.preventDefault();
e.dataTransfer!.dropEffect = "move";
const target = e.target as HTMLElement;
const doc = target.ownerDocument;
const draggedNode = doc.querySelector(".dragging");
if (!draggedNode) return;
// 找到最近的书签节点元素
const targetNode = target.closest(".bookmark-node");
if (!targetNode) {
hideBookmarkDropIndicator(doc);
return;
}
// 不能拖拽到自己
const targetLi = targetNode.closest("li") as Element;
if (draggedNode === targetLi) {
hideBookmarkDropIndicator(doc);
return;
}
// 计算拖拽位置(上方或下方)
const rect = targetNode.getBoundingClientRect();
const mouseY = e.clientY;
const relativeY = mouseY - rect.top;
const height = rect.height;
let dropPosition;
if (relativeY < height * 0.5) {
dropPosition = "before";
} else {
dropPosition = "after";
}
// 更新指示器
updateBookmarkDropIndicator(targetNode, dropPosition);
// 添加可放置样式
doc.querySelectorAll(".bookmark-dragover").forEach((el) => {
el.classList.remove("bookmark-dragover");
});
targetNode.classList.add("bookmark-dragover");
}
// 更新书签拖拽指示器
function updateBookmarkDropIndicator(targetNode: Element, position: string) {
const rect = targetNode.getBoundingClientRect();
const doc = targetNode.ownerDocument;
const dropIndicator = doc.querySelector(
".bookmark-drop-indicator",
) as HTMLElement;
dropIndicator.classList.add("visible");
if (position === "before") {
dropIndicator.style.left = `${rect.left}px`;
dropIndicator.style.top = `${rect.top - 2}px`;
dropIndicator.style.width = `${rect.width}px`;
} else {
// after position
dropIndicator.style.left = `${rect.left}px`;
dropIndicator.style.top = `${rect.bottom}px`;
dropIndicator.style.width = `${rect.width}px`;
}
}
// 隐藏书签拖拽指示器
function hideBookmarkDropIndicator(doc: Document) {
const dropIndicator = doc.querySelector(".bookmark-drop-indicator")!;
dropIndicator.classList.remove("visible");
}
// 书签拖拽离开目标元素
export function handleBookmarkDragLeave(e: DragEvent) {
const doc = (e.target as Element).ownerDocument;
if (
!e.relatedTarget ||
!(e.relatedTarget as Element).closest("#j-bookmark-viewer")
) {
hideBookmarkDropIndicator(doc);
}
const targetNode = (e.target as HTMLElement).closest(".bookmark-node");
if (targetNode) {
targetNode.classList.remove("bookmark-dragover");
}
}
// 处理书签放置
export async function handleBookmarkDrop(e: DragEvent) {
e.preventDefault();
const target = e.target as HTMLElement;
const doc = target.ownerDocument;
const draggedNode = doc.querySelector(".dragging");
// 隐藏指示器
hideBookmarkDropIndicator(doc);
if (!draggedNode) return;
// 获取目标节点
const targetBookmarkNode = target.closest(".bookmark-node");
if (!targetBookmarkNode) return;
// 移除可放置样式
doc.querySelectorAll(".bookmark-dragover").forEach((el) => {
el.classList.remove("bookmark-dragover");
});
// 获取目标列表项
const targetLi = targetBookmarkNode.closest("li")!;
// 不能将节点拖到自己上
if (draggedNode === targetLi) {
return;
}
// 移除拖拽的节点
const oldParent = draggedNode.parentNode! as HTMLElement;
oldParent.removeChild(draggedNode);
// 判断放置位置
const rect = targetBookmarkNode.getBoundingClientRect();
const mouseY = e.clientY;
const relativeY = mouseY - rect.top;
const height = rect.height;
const targetParent = targetLi.parentNode!;
if (relativeY < height * 0.5) {
// 放在前面
targetParent.insertBefore(draggedNode, targetLi);
} else {
// 放在后面
targetParent.insertBefore(draggedNode, targetLi.nextSibling);
}
// 保存书签信息
await saveBookmarksToJSON();
}
// 书签拖拽结束
export function handleBookmarkDragEnd(e: DragEvent) {
const doc = (e.target as HTMLElement).ownerDocument;
const draggedNode = doc.querySelector(".dragging");
if (!draggedNode) return;
draggedNode.classList.remove("dragging");
// 隐藏指示器
hideBookmarkDropIndicator(doc);
// 清除所有dragover样式
doc.querySelectorAll(".bookmark-dragover").forEach((el) => {
el.classList.remove("bookmark-dragover");
});
}
// ========== 字体大小调整函数 ==========
const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 20;
// Increase font size for both outline and bookmark
async function handleFontSizeIncrease(ev: Event) {
const doc = (ev.target as Element).ownerDocument;
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
if (!reader) return;
// Get current baseFontSize for outline
const outlineInfo = await loadOutlineInfoFromJSON(reader._item);
const currentOutlineSize =
outlineInfo?.baseFontSize ?? DEFAULT_BASE_FONT_SIZE;
// Get current baseFontSize for bookmark
const bookmarkInfo = await loadBookmarkInfoFromJSON(reader._item);
const currentBookmarkSize =
bookmarkInfo?.baseFontSize ?? DEFAULT_BOOKMARK_FONT_SIZE;
// Increase by 1, max 20
const newOutlineSize = Math.min(currentOutlineSize + 1, MAX_FONT_SIZE);
const newBookmarkSize = Math.min(currentBookmarkSize + 1, MAX_FONT_SIZE);
if (
newOutlineSize !== currentOutlineSize ||
newBookmarkSize !== currentBookmarkSize
) {
// Update CSS
updateOutlineFontSize(doc, newOutlineSize);
updateBookmarkFontSize(doc, newBookmarkSize);
// Save to JSON
await saveOutlineToJSON(reader._item, undefined, newOutlineSize);
await saveBookmarksToJSON(reader._item, undefined, newBookmarkSize);
ztoolkit.log(
`Font size increased: outline=${newOutlineSize}px, bookmark=${newBookmarkSize}px`,
);
}
}
// Decrease font size for both outline and bookmark
async function handleFontSizeDecrease(ev: Event) {
const doc = (ev.target as Element).ownerDocument;
const reader = Zotero.Reader.getByTabID(
ztoolkit.getGlobal("Zotero_Tabs").selectedID,
);
if (!reader) return;
// Get current baseFontSize for outline
const outlineInfo = await loadOutlineInfoFromJSON(reader._item);
const currentOutlineSize =
outlineInfo?.baseFontSize ?? DEFAULT_BASE_FONT_SIZE;
// Get current baseFontSize for bookmark
const bookmarkInfo = await loadBookmarkInfoFromJSON(reader._item);
const currentBookmarkSize =
bookmarkInfo?.baseFontSize ?? DEFAULT_BOOKMARK_FONT_SIZE;
// Decrease by 1, min 8
const newOutlineSize = Math.max(currentOutlineSize - 1, MIN_FONT_SIZE);
const newBookmarkSize = Math.max(currentBookmarkSize - 1, MIN_FONT_SIZE);
if (
newOutlineSize !== currentOutlineSize ||
newBookmarkSize !== currentBookmarkSize
) {
// Update CSS
updateOutlineFontSize(doc, newOutlineSize);
updateBookmarkFontSize(doc, newBookmarkSize);
// Save to JSON
await saveOutlineToJSON(reader._item, undefined, newOutlineSize);
await saveBookmarksToJSON(reader._item, undefined, newBookmarkSize);
ztoolkit.log(
`Font size decreased: outline=${newOutlineSize}px, bookmark=${newBookmarkSize}px`,
);
}
}
================================================
FILE: src/modules/outline/index.ts
================================================
import { wait } from "zotero-plugin-toolkit";
import { getString } from "../../utils/locale";
import { initEventListener } from "./events";
import {
addButton,
createTreeNodes,
getOutlineFromPDF,
registerOutlineCSS,
registerThemeChange,
updateOutlineFontSize,
loadOutlineInfoFromJSON,
DEFAULT_BASE_FONT_SIZE,
} from "./outline";
import {
addBookmarkButton,
createBookmarkNodes,
loadBookmarksFromJSON,
updateBookmarkFontSize,
loadBookmarkInfoFromJSON,
DEFAULT_BOOKMARK_FONT_SIZE,
} from "./bookmark";
import { ICONS } from "./style";
import { getPref } from "../../utils/prefs";
export function renderTree(
reader: _ZoteroTypes.ReaderInstance,
doc: Document,
data: OutlineNode[] | null,
) {
const dropIndicator = ztoolkit.UI.createElement(doc, "div", {
classList: ["drop-indicator"],
});
const toolbar = ztoolkit.UI.createElement(doc, "div", {
namespace: "html",
id: "j-outline-toolbar",
classList: ["j-hidden"], // 默认隐藏
children: [
{
tag: "button",
id: "j-outline-expand-all",
classList: ["j-outline-toolbar-button", "toolbar-button"],
properties: { innerHTML: ICONS.expand },
attributes: { title: getString("outline-expand-all") },
},
{
tag: "button",
id: "j-outline-collapse-all",
classList: ["j-outline-toolbar-button", "toolbar-button"],
properties: { innerHTML: ICONS.collapse },
attributes: { title: getString("outline-collapse-all") },
},
{
tag: "button",
id: "j-outline-add-node",
classList: ["j-outline-toolbar-button", "toolbar-button"],
properties: { innerHTML: ICONS.add },
attributes: { title: getString("outline-add") },
},
{
tag: "button",
id: "j-outline-delete-node",
classList: ["j-outline-toolbar-button", "toolbar-button"],
properties: { innerHTML: ICONS.del },
attributes: { title: getString("outline-delete") },
},
{
tag: "button",
id: "j-outline-save-pdf",
classList: ["j-outline-toolbar-button", "toolbar-button"],
properties: { innerHTML: ICONS.save },
attributes: { title: getString("outline-save-to-pdf") },
},
],
});
const treeContainer = ztoolkit.UI.createElement(doc, "div", {
id: "jasminum-outline",
classList: ["hidden"], // 默认隐藏
namespace: "html",
children: [
{
tag: "div",
namespace: "html",
id: "j-outline-viewer",
classList: ["outline-view"],
attributes: {
tabindex: "-1",
"data-tabstop": "1",
role: "tabpanel",
"aria-labelledby": "j-outline-button",
},
children: [
{
tag: "ul",
namespace: "html",
id: "root-list",
classList: ["tree-list"],
},
],
},
{
tag: "div",
namespace: "html",
classList: ["jasminum-sidebar-bottom"],
children: [
{
tag: "button",
namespace: "html",
id: "j-outline-zoom-in",
classList: ["j-outline-toolbar-button", "toolbar-button"],
properties: { innerHTML: ICONS.plus },
attributes: { title: "字体变大" },
styles: { paddingBottom: "7px" },
},
{
tag: "button",
namespace: "html",
id: "j-outline-zoom-out",
classList: ["j-outline-toolbar-button", "toolbar-button"],
properties: { innerHTML: ICONS.minus },
attributes: { title: "字体变小" },
styles: { paddingBottom: "7px" },
},
],
},
],
});
// 隐藏 Zotero 大纲按钮
if (getPref("disableZoteroOutline")) {
doc.getElementById("viewOutline")!.style.display = "none";
}
// 添加工具栏
doc
.getElementById("sidebarContainer")!
.insertBefore(toolbar, doc.getEl
gitextract_udbmtm46/ ├── .gitattributes ├── .github/ │ ├── dependabot.yml │ ├── renovate.json │ └── workflows/ │ └── release.yml ├── .gitignore ├── .prettierignore ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── toolkit.code-snippets ├── LICENSE ├── README.md ├── addon/ │ ├── bootstrap.js │ ├── chrome/ │ │ └── content/ │ │ ├── preferences-main.xhtml │ │ ├── preferences-translators.xhtml │ │ ├── prefpanel.css │ │ └── progress.xhtml │ ├── locale/ │ │ ├── en-US/ │ │ │ ├── addon.ftl │ │ │ ├── mainWindow.ftl │ │ │ ├── preferences-main.ftl │ │ │ ├── preferences-translators.ftl │ │ │ └── progress.ftl │ │ ├── zh-CN/ │ │ │ ├── addon.ftl │ │ │ ├── mainWindow.ftl │ │ │ ├── preferences-main.ftl │ │ │ ├── preferences-translators.ftl │ │ │ └── progress.ftl │ │ └── zh-TW/ │ │ ├── addon.ftl │ │ ├── mainWindow.ftl │ │ ├── preferences-main.ftl │ │ ├── preferences-translators.ftl │ │ └── progress.ftl │ ├── manifest.json │ └── prefs.js ├── doc/ │ └── README-zhCN.md ├── eslint.config.mjs ├── package.json ├── src/ │ ├── addon.ts │ ├── hooks.ts │ ├── index.ts │ ├── modules/ │ │ ├── attachments/ │ │ │ ├── index.ts │ │ │ └── localMatch.ts │ │ ├── menu.ts │ │ ├── notifier.ts │ │ ├── outline/ │ │ │ ├── bookmark.ts │ │ │ ├── events.ts │ │ │ ├── index.ts │ │ │ ├── outline.ts │ │ │ └── style.ts │ │ ├── preferences/ │ │ │ ├── main.ts │ │ │ └── translators.ts │ │ ├── progress.ts │ │ ├── services/ │ │ │ ├── cnki.ts │ │ │ ├── index.ts │ │ │ ├── pubscholar.ts │ │ │ └── yiigle.ts │ │ ├── styles.ts │ │ ├── tools.ts │ │ ├── translators.ts │ │ ├── workers/ │ │ │ ├── index.ts │ │ │ └── outline.ts │ │ └── wps.ts │ └── utils/ │ ├── cookiebox.ts │ ├── detect.ts │ ├── http.ts │ ├── locale.ts │ ├── pattern.ts │ ├── pdfParser.ts │ ├── prefs.ts │ ├── task.ts │ ├── wait.ts │ ├── window.ts │ └── ztoolkit.ts ├── test/ │ ├── CNKI_translator_test.js │ ├── expert_china.json │ └── expert_oversea.json ├── tsconfig.json ├── typings/ │ ├── attachment.d.ts │ ├── global.d.ts │ ├── i10n.d.ts │ ├── myzotero.d.ts │ ├── notifier.d.ts │ ├── outline.d.ts │ ├── pdfParser.d.ts │ ├── prefs.d.ts │ ├── scrape.d.ts │ └── translators.d.ts └── zotero-plugin.config.ts
SYMBOL INDEX (295 symbols across 46 files)
FILE: addon/bootstrap.js
function install (line 12) | function install(data, reason) {}
function startup (line 14) | async function startup({ id, version, resourceURI, rootURI }, reason) {
function onMainWindowLoad (line 48) | async function onMainWindowLoad({ window }, reason) {
function onMainWindowUnload (line 52) | async function onMainWindowUnload({ window }, reason) {
function shutdown (line 56) | function shutdown({ id, version, resourceURI, rootURI }, reason) {
function uninstall (line 80) | function uninstall(data, reason) {}
FILE: src/addon.ts
class Addon (line 10) | class Addon {
method constructor (line 41) | constructor() {
FILE: src/hooks.ts
function onStartup (line 19) | async function onStartup() {
function onMainWindowLoad (line 46) | async function onMainWindowLoad(win: Window): Promise<void> {
function onShutdown (line 61) | function onShutdown(): void {
FILE: src/index.ts
function defineGlobal (line 18) | function defineGlobal(name: string, getter?: () => any) {
FILE: src/modules/attachments/index.ts
function attachmentSearch (line 5) | async function attachmentSearch(task: AttachmentTask): Promise<void> {
function importAttachment (line 24) | async function importAttachment(task: AttachmentTask): Promise<void> {
function actionAfterImport (line 32) | async function actionAfterImport(
FILE: src/modules/attachments/localMatch.ts
function findAttachmentsInFolder (line 6) | async function findAttachmentsInFolder(
class LocalAttachmentService (line 17) | class LocalAttachmentService implements AttachmentService {
method searchAttachments (line 18) | async searchAttachments(
method importAttachment (line 66) | async importAttachment(task: AttachmentTask): Promise<void> {
FILE: src/modules/menu.ts
function registerMenu (line 93) | function registerMenu() {
FILE: src/modules/notifier.ts
function registerNotifier (line 12) | function registerNotifier(
function unregisterNotifier (line 46) | function unregisterNotifier(notifierID: string) {
function registerNotifiers (line 53) | function registerNotifiers() {
function onAddItem (line 58) | async function onAddItem(
function registerExtraColumnWithCustomCell (line 103) | async function registerExtraColumnWithCustomCell() {
function registerTab (line 124) | function registerTab() {
function tabRegisterCallback (line 140) | async function tabRegisterCallback(event: any) {
FILE: src/modules/outline/bookmark.ts
constant BOOKMARK_SCHEMA (line 6) | const BOOKMARK_SCHEMA = OUTLINE_SCHEMA;
constant DEFAULT_BOOKMARK_FONT_SIZE (line 7) | const DEFAULT_BOOKMARK_FONT_SIZE = 13;
constant DEFAULT_BOOKMARK_COLORS (line 10) | const DEFAULT_BOOKMARK_COLORS = [
function getRandomBookmarkColor (line 26) | function getRandomBookmarkColor(): string {
function migrateBookmarkInfo (line 33) | function migrateBookmarkInfo(
function getReaderPagePosition (line 54) | function getReaderPagePosition(): PdfPosition {
function saveBookmarksToJSON (line 72) | async function saveBookmarksToJSON(
function loadBookmarkInfoFromJSON (line 111) | async function loadBookmarkInfoFromJSON(
function loadBookmarksFromJSON (line 150) | async function loadBookmarksFromJSON(
function getBookmarksFromPage (line 157) | function getBookmarksFromPage(): BookmarkNode[] {
function createBookmarkNodes (line 188) | function createBookmarkNodes(
function generateSmartBookmarkTitle (line 254) | function generateSmartBookmarkTitle(pageNumber: number): string {
function addNewBookmark (line 272) | function addNewBookmark(title?: string): BookmarkNode {
function addBookmarkButton (line 288) | function addBookmarkButton(doc: Document) {
function updateBookmarkFontSize (line 312) | function updateBookmarkFontSize(doc: Document, baseFontSize: number) {
FILE: src/modules/outline/events.ts
constant MAX_LEVEL (line 22) | const MAX_LEVEL = 7;
function getReaderPagePosition (line 24) | function getReaderPagePosition(): PdfPosition {
function initEventListener (line 52) | function initEventListener(
function expandAll (line 234) | async function expandAll(ev: Event) {
function collapseAll (line 249) | async function collapseAll(ev: Event) {
function toggleNode (line 264) | function toggleNode(node: Element) {
function selectNode (line 281) | function selectNode(node: Element) {
function handleKeydownEvent (line 294) | async function handleKeydownEvent(ev: KeyboardEvent) {
function handleDragStart (line 409) | function handleDragStart(e: DragEvent) {
function handleDragOver (line 426) | function handleDragOver(e: DragEvent) {
function updateDropIndicator (line 485) | function updateDropIndicator(
function hideDropIndicator (line 517) | function hideDropIndicator(doc: Document) {
function handleDragLeave (line 525) | function handleDragLeave(e: DragEvent) {
function handleDrop (line 542) | async function handleDrop(e: DragEvent) {
function handleDragEnd (line 631) | function handleDragEnd(e: DragEvent) {
function isAncestor (line 649) | function isAncestor(ancestor: Element, descendant: Element) {
function determineDropPosition (line 661) | function determineDropPosition(event: DragEvent, targetNode: Element) {
function updateNodeLevels (line 679) | function updateNodeLevels(node: Element) {
function makeNodeEditable (line 715) | function makeNodeEditable(titleElement: Element) {
function deleteSelectedNode (line 786) | async function deleteSelectedNode(ev: Event) {
function addNewNode (line 851) | async function addNewNode(ev: Event) {
function clickToPosition (line 922) | function clickToPosition(targetElement: Element) {
function addOutlineToPDFRunner (line 955) | async function addOutlineToPDFRunner(): Promise<void> {
function selectBookmarkNode (line 1009) | function selectBookmarkNode(node: Element) {
function clickToBookmarkPosition (line 1021) | function clickToBookmarkPosition(targetElement: Element) {
function makeBookmarkNodeEditable (line 1050) | function makeBookmarkNodeEditable(titleElement: Element) {
function addNewBookmarkNode (line 1162) | async function addNewBookmarkNode(ev: Event) {
function deleteSelectedBookmarkNode (line 1177) | async function deleteSelectedBookmarkNode(ev: Event) {
function handleBookmarkDragStart (line 1208) | function handleBookmarkDragStart(e: DragEvent) {
function handleBookmarkDragOver (line 1224) | function handleBookmarkDragOver(e: DragEvent) {
function updateBookmarkDropIndicator (line 1270) | function updateBookmarkDropIndicator(targetNode: Element, position: stri...
function hideBookmarkDropIndicator (line 1292) | function hideBookmarkDropIndicator(doc: Document) {
function handleBookmarkDragLeave (line 1298) | function handleBookmarkDragLeave(e: DragEvent) {
function handleBookmarkDrop (line 1314) | async function handleBookmarkDrop(e: DragEvent) {
function handleBookmarkDragEnd (line 1367) | function handleBookmarkDragEnd(e: DragEvent) {
constant MIN_FONT_SIZE (line 1385) | const MIN_FONT_SIZE = 8;
constant MAX_FONT_SIZE (line 1386) | const MAX_FONT_SIZE = 20;
function handleFontSizeIncrease (line 1389) | async function handleFontSizeIncrease(ev: Event) {
function handleFontSizeDecrease (line 1429) | async function handleFontSizeDecrease(ev: Event) {
FILE: src/modules/outline/index.ts
function renderTree (line 25) | function renderTree(
function renderBookmarkTree (line 142) | function renderBookmarkTree(
function addOutlineToReader (line 214) | async function addOutlineToReader(reader: _ZoteroTypes.ReaderInstance) {
function registerOutline (line 263) | async function registerOutline(tabID: string) {
FILE: src/modules/outline/outline.ts
constant OUTLINE_SCHEMA (line 7) | const OUTLINE_SCHEMA = 2;
constant DEFAULT_BASE_FONT_SIZE (line 8) | const DEFAULT_BASE_FONT_SIZE = 12;
function registerOutlineCSS (line 11) | function registerOutlineCSS(doc: Document) {
function updateOutlineFontSize (line 27) | function updateOutlineFontSize(doc: Document, baseFontSize: number) {
function registerThemeChange (line 56) | function registerThemeChange(win: Window) {
function addButton (line 75) | function addButton(doc: Document) {
function getOutlineFromPDF (line 145) | async function getOutlineFromPDF(
function getOutlineFromPage (line 220) | function getOutlineFromPage(): OutlineNode[] {
function saveOutlineToJSON (line 251) | async function saveOutlineToJSON(
function migrateOutlineInfo (line 290) | function migrateOutlineInfo(
function loadOutlineInfoFromJSON (line 308) | async function loadOutlineInfoFromJSON(
function loadOutlineFromJSON (line 340) | async function loadOutlineFromJSON(
function createTreeNodes (line 347) | function createTreeNodes(
FILE: src/modules/outline/style.ts
constant ICONS (line 1) | const ICONS = {
FILE: src/modules/preferences/main.ts
function registerPrefsPane (line 9) | function registerPrefsPane() {
function onPrefsWindowLoad (line 23) | async function onPrefsWindowLoad(_window: Window) {
function initPrefs (line 38) | async function initPrefs() {
function migratePrefs (line 87) | function migratePrefs(prefix: string) {
function updatePrefsUI (line 133) | async function updatePrefsUI(doc: Document) {
function bindPrefEvents (line 147) | function bindPrefEvents(doc: Document) {
FILE: src/modules/preferences/translators.ts
function onWindowLoad (line 6) | async function onWindowLoad(_window: Window) {
function updateRowData (line 63) | async function updateRowData() {
function updateTableUI (line 78) | async function updateTableUI() {
function bindEvents (line 86) | function bindEvents(doc: Document) {
function onShowTable (line 132) | async function onShowTable() {
FILE: src/modules/progress.ts
class Progress (line 7) | class Progress {
method constructor (line 11) | constructor() {
method openProgressWindow (line 22) | public async openProgressWindow(): Promise<void> {
method createSearchResultProps (line 59) | private createSearchResultProps(
method addTaskToProgressWindow (line 120) | public async addTaskToProgressWindow(task: Task): Promise<void> {
method linkifyMessage (line 187) | private linkifyMessage(doc: Document, message: string): DocumentFragme...
method updateTaskStatus (line 233) | public updateTaskStatus(task: Task, status: string): void {
method updateTaskSearchResult (line 313) | public updateTaskSearchResult(
FILE: src/modules/services/cnki.ts
function createSearchPostOptions (line 11) | function createSearchPostOptions(searchOption: SearchOption) {
function getRefworksText (line 176) | async function getRefworksText(
function getSnapshotItem (line 250) | async function getSnapshotItem(
function updateItem (line 275) | async function updateItem(
class CNKI (line 311) | class CNKI implements ScrapeService {
method search (line 312) | async search(
method translate (line 384) | async translate(
method searchSnapshot (line 451) | async searchSnapshot(
FILE: src/modules/services/index.ts
function getSearchOption (line 15) | async function getSearchOption(
function metaSearch (line 37) | async function metaSearch(
function metaTranslate (line 121) | async function metaTranslate(task: ScraperTask): Promise<void> {
function globalItemFix (line 206) | async function globalItemFix(
FILE: src/modules/services/pubscholar.ts
constant BASE_URL (line 5) | const BASE_URL = "https://pubscholar.cn";
function parseSearchResults (line 10) | function parseSearchResults(doc: Document): ScrapeSearchResult[] {
function createItemFromMetadata (line 37) | async function createItemFromMetadata(
class PubScholar (line 53) | class PubScholar implements ScrapeService {
method search (line 54) | async search(
method translate (line 84) | async translate(
FILE: src/modules/services/yiigle.ts
class Yiigle (line 7) | class Yiigle implements ScrapeService {
method search (line 8) | async search(
method translate (line 76) | async translate(
FILE: src/modules/styles.ts
function injectToDocument (line 6) | function injectToDocument(doc: Document) {
function injectStylesLink (line 82) | async function injectStylesLink() {
FILE: src/modules/tools.ts
function splitName (line 171) | async function splitName(item: Zotero.Item): Promise<void> {
function mergeName (line 207) | async function mergeName(item: Zotero.Item): Promise<void> {
function getCNKICite (line 236) | async function getCNKICite(item: Zotero.Item): Promise<string> {
function updateCNKICite (line 254) | async function updateCNKICite(items: Zotero.Item[]) {
function renameAttachmentFromParent (line 294) | async function renameAttachmentFromParent(attachmentItem: Zotero.Item) {
function importAttachmentsFromFolder (line 335) | async function importAttachmentsFromFolder(): Promise<void> {
function handleAttachmentMenu (line 388) | async function handleAttachmentMenu(menuType: "collection" | "item") {
FILE: src/modules/translators.ts
function bestSpeedBaseUrl (line 4) | async function bestSpeedBaseUrl() {
function getLastUpdatedFromFile (line 42) | async function getLastUpdatedFromFile(
function getLastUpdatedMap (line 70) | async function getLastUpdatedMap(
function mendTranslators (line 97) | async function mendTranslators() {
function updateTranslators (line 124) | async function updateTranslators(force = false): Promise<boolean> {
function _updateTranslators (line 139) | async function _updateTranslators(force = false): Promise<boolean> {
FILE: src/modules/workers/outline.ts
function test (line 13) | function test(title: string) {
function prepareData (line 27) | function prepareData(
function createOutlineItem (line 42) | function createOutlineItem(
function createOutlineDict (line 88) | function createOutlineDict(
function addOutlineToPDF (line 104) | async function addOutlineToPDF(
FILE: src/modules/wps.ts
function unZip (line 1) | async function unZip(filename: string, outDir: string) {
function downloadWpsPlugin (line 38) | async function downloadWpsPlugin() {
function installWpsPlugin (line 55) | async function installWpsPlugin() {
FILE: src/utils/cookiebox.ts
class MyCookieSandbox (line 1) | class MyCookieSandbox {
method constructor (line 15) | constructor() {
method getCookieBoxFromUrl (line 19) | public async getCookieBoxFromUrl(
method getCNKIHomeCookieBox (line 225) | public async getCNKIHomeCookieBox(): Promise<Zotero.CookieSandbox> {
method passCaptchaToCookieBox (line 266) | async passCaptchaToCookieBox(
FILE: src/utils/detect.ts
constant CHINESE_FILENAME_REGEX (line 10) | const CHINESE_FILENAME_REGEX =
function isChineseAttachmentFilename (line 12) | function isChineseAttachmentFilename(filename: string): boolean {
function isChineseTopAttachment (line 19) | function isChineseTopAttachment(item: Zotero.Item): boolean {
function isChineseTopItem (line 32) | function isChineseTopItem(item: Zotero.Item): boolean {
function isChinsesSnapshot (line 46) | function isChinsesSnapshot(item: Zotero.Item): boolean {
FILE: src/utils/http.ts
function jsonToFormUrlEncoded (line 1) | function jsonToFormUrlEncoded(json: any) {
function requestDocument (line 14) | async function requestDocument(
function text2HTMLDoc (line 37) | function text2HTMLDoc(text: string, url?: string): Document {
function isMainlandChina (line 47) | async function isMainlandChina(): Promise<boolean> {
class DocTools (line 95) | class DocTools {
method constructor (line 97) | constructor(node: Document | Element) {
method attr (line 100) | attr(selector: string, attr: string, index?: number): string {
method text (line 104) | text(selector: string, index?: number): string {
method innerText (line 108) | innerText(selector: string, index?: number): string {
method choose (line 112) | choose(selector: string, index?: number): Element | null {
FILE: src/utils/locale.ts
function initLocale (line 8) | function initLocale() {
function getString (line 48) | function getString(...inputs: any[]) {
function _getString (line 62) | function _getString(
function getLocaleID (line 86) | function getLocaleID(id: string) {
FILE: src/utils/pattern.ts
function getArgsFromPattern (line 1) | function getArgsFromPattern(
FILE: src/utils/pdfParser.ts
function getPDFTitle (line 1) | async function getPDFTitle(itemID: number): Promise<string> {
function isValidTitle (line 23) | function isValidTitle(line: PdfParagraph): boolean {
function getThesisTitle (line 29) | function getThesisTitle(data: PdfData): string {
function getArticleTitle (line 42) | function getArticleTitle(data: PdfData): string {
function findParagraphInPages (line 54) | function findParagraphInPages(
function findParagraphInPagesReversed (line 71) | function findParagraphInPagesReversed(
function findParagraphAfter (line 88) | function findParagraphAfter(paragraphs: PdfParagraph[], patterns: RegExp...
function findParagraphBefore (line 99) | function findParagraphBefore(paragraphs: PdfParagraph[], patterns: RegEx...
function findMaxSizeParagraph (line 110) | function findMaxSizeParagraph(paragraphs: PdfParagraph[]) {
function detectDocType (line 125) | function detectDocType(data: PdfData): DocType {
function sortLines (line 162) | function sortLines(lines: PdfLine[]): PdfLine[] {
function recognizerDataToPdfData (line 171) | function recognizerDataToPdfData(data: RecognizerData): PdfData {
function recognizerPageToPdfPage (line 179) | function recognizerPageToPdfPage(page: RecognizerPage): PdfPage {
function pdfLinesToPdfParagraphs (line 195) | function pdfLinesToPdfParagraphs(lines: PdfLine[]): PdfParagraph[] {
function recognizerLineToPdfLine (line 273) | function recognizerLineToPdfLine(line: RecognizerLine): PdfLine {
function pdfWordsToPdfLine (line 278) | function pdfWordsToPdfLine(words: PdfWord[]): PdfLine {
function recognizerWordToPdfWord (line 298) | function recognizerWordToPdfWord(word: RecognizerWord): PdfWord {
function average (line 317) | function average<T>(arr: T[], callback: (arg: T) => number): number {
function hasCJK (line 321) | function hasCJK(str: string) {
function xnor (line 325) | function xnor(input1: boolean, input2: boolean) {
function debugDoc (line 329) | function debugDoc(data: RecognizerData) {
function patternsInType (line 396) | function patternsInType(type: DocType): RegExp[] {
function normalizeText (line 489) | function normalizeText(str: string) {
FILE: src/utils/prefs.ts
type PluginPrefsMap (line 3) | type PluginPrefsMap = _ZoteroTypes.Prefs["PluginPrefsMap"];
constant PREFS_PREFIX (line 5) | const PREFS_PREFIX = config.prefsPrefix;
function getPref (line 12) | function getPref<K extends keyof PluginPrefsMap>(key: K) {
function setPref (line 22) | function setPref<K extends keyof PluginPrefsMap>(
function clearPref (line 34) | function clearPref(key: string) {
FILE: src/utils/task.ts
function createDeferred (line 7) | function createDeferred<T>(): DeferredResult<T> {
class ScraperTask (line 19) | class ScraperTask implements ScraperTask {
method constructor (line 31) | constructor(item: Zotero.Item, type: ScraperTaskType, silent?: false) {
method addMsg (line 40) | addMsg(message: string) {
method status (line 49) | set status(newStatus: TaskStatus) {
method status (line 61) | get status(): TaskStatus {
method searchResults (line 65) | set searchResults(results: ScrapeSearchResult[]) {
method searchResults (line 72) | get searchResults() {
class AttachmentTask (line 77) | class AttachmentTask implements AttachmentTask {
method constructor (line 89) | constructor(item: Zotero.Item, type: AttachmentTaskType, silent?: fals...
method addMsg (line 98) | addMsg(message: string) {
method status (line 107) | set status(newStatus: TaskStatus) {
method status (line 119) | get status(): TaskStatus {
method searchResults (line 123) | set searchResults(results: AttachmentSearchResult[]) {
method searchResults (line 130) | get searchResults() {
class TaskRunner (line 135) | class TaskRunner {
method getTaskType (line 138) | getTaskType(
method createTask (line 156) | createTask(
method addTask (line 178) | async addTask(
method createAndAddTask (line 199) | async createAndAddTask(
method getTaskById (line 211) | getTaskById(id: string): Task | undefined {
method runTask (line 215) | async runTask(task: AttachmentTask | ScraperTask): Promise<void> {
method runScrapeTask (line 225) | async runScrapeTask(task: ScraperTask): Promise<void> {
method runAttachmentTask (line 245) | async runAttachmentTask(task: AttachmentTask): Promise<void> {
method resumeTask (line 256) | resumeTask(taskID: string, resultIndex: number): void {
FILE: src/utils/wait.ts
function waitUntil (line 9) | function waitUntil(
function waitUtilAsync (line 32) | function waitUtilAsync(
FILE: src/utils/window.ts
function isWindowAlive (line 9) | function isWindowAlive(win?: Window) {
function waitNoMoreThan (line 20) | async function waitNoMoreThan<T>(
function findWindow (line 40) | function findWindow(type: string) {
function observeWindowLoad (line 52) | function observeWindowLoad(
function waitElmLoaded (line 89) | async function waitElmLoaded(
FILE: src/utils/ztoolkit.ts
function createZToolkit (line 6) | function createZToolkit() {
function initZToolkit (line 17) | function initZToolkit(_ztoolkit: ReturnType<typeof createZToolkit>) {
class MyToolkit (line 35) | class MyToolkit extends BasicTool {
method constructor (line 38) | constructor() {
method unregisterAll (line 43) | unregisterAll() {
FILE: test/CNKI_translator_test.js
function getDoc (line 14) | async function getDoc(url) {
function translate (line 25) | async function translate(doc) {
function delay (line 38) | function delay(ms) {
function test (line 42) | async function test(urls) {
FILE: typings/attachment.d.ts
type AttachmentService (line 1) | interface AttachmentService {
type AttachmentSearchResult (line 8) | type AttachmentSearchResult = {
type AttachmentTask (line 16) | interface AttachmentTask extends Task {
FILE: typings/global.d.ts
type ZToolkit (line 8) | type ZToolkit = ReturnType<
FILE: typings/i10n.d.ts
type FluentMessageId (line 5) | type FluentMessageId =
FILE: typings/myzotero.d.ts
type CookieData (line 5) | interface CookieData {
type CookieStorage (line 18) | interface CookieStorage {
type CookieDict (line 30) | interface CookieDict {
class CookieSandbox (line 37) | class CookieSandbox {
FILE: typings/outline.d.ts
type OutlineNode (line 1) | type OutlineNode = {
type OutlineInfo (line 12) | type OutlineInfo = {
type PdfZoomMode (line 26) | type PdfZoomMode = {
type PdfDest (line 31) | type PdfDest = { dest: [PDFRef, PdfZoomMode] };
type PdfPosition (line 32) | type PdfPosition = {
type PdfOutlineNode (line 36) | type PdfOutlineNode = {
type BookmarkNode (line 43) | type BookmarkNode = {
type BookmarkInfo (line 54) | type BookmarkInfo = {
FILE: typings/pdfParser.d.ts
type RecognizerData (line 1) | type RecognizerData = {
type PdfData (line 9) | type PdfData = {
type RecognizerPage (line 17) | type RecognizerPage = {
type PdfPage (line 25) | type PdfPage = {
type PdfParagraph (line 33) | type PdfParagraph = {
type RecognizerLine (line 40) | type RecognizerLine = [RecognizerWord[]];
type PdfLine (line 42) | type PdfLine = {
type RecognizerWord (line 53) | type RecognizerWord = [
type PdfWord (line 84) | type PdfWord = {
type DocType (line 101) | type DocType = "article" | "thesis" | "book";
FILE: typings/prefs.d.ts
type Prefs (line 8) | interface Prefs {
FILE: typings/scrape.d.ts
type ScrapeService (line 1) | interface ScrapeService {
type SearchOption (line 12) | type SearchOption = {
type ScrapeSearchResult (line 17) | type ScrapeSearchResult = {
type TaskStatus (line 24) | type TaskStatus =
type ScraperTaskType (line 30) | type ScraperTaskType = "attachment" | "snapshot";
type AttachmentTaskType (line 31) | type AttachmentTaskType = "local" | "remote";
type Task (line 32) | interface Task {
type ScrapeTask (line 45) | interface ScrapeTask extends Task {
type DeferredResult (line 51) | type DeferredResult<T = any> = {
FILE: typings/translators.d.ts
type LastUpdatedMap (line 1) | type LastUpdatedMap = {
type TableRow (line 5) | type TableRow = {
Condensed preview — 88 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (406K chars).
[
{
"path": ".gitattributes",
"chars": 18,
"preview": "* text=auto eol=lf"
},
{
"path": ".github/dependabot.yml",
"chars": 502,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/renovate.json",
"chars": 609,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"config:recommended\",\n \":seman"
},
{
"path": ".github/workflows/release.yml",
"chars": 1558,
"preview": "name: Release\n\non:\n push:\n tags:\n - v**\n workflow_dispatch: # 新增手动触发入口\n inputs:\n version:\n de"
},
{
"path": ".gitignore",
"chars": 79,
"preview": "build\nlogs\nnode_modules\npnpm-lock.yaml\nyarn.lock\n.DS_Store\n.env\n.scaffold\ntmp/\n"
},
{
"path": ".prettierignore",
"chars": 75,
"preview": ".vscode\nbuild\nlogs\nnode_modules\npackage-lock.json\nyarn.lock\npnpm-lock.yaml\n"
},
{
"path": ".vscode/extensions.json",
"chars": 120,
"preview": "{\n \"recommendations\": [\n \"dbaeumer.vscode-eslint\",\n \"esbenp.prettier-vscode\",\n \"macabeus.vscode-fluent\"\n ]\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 478,
"preview": "{\n // 使用 IntelliSense 了解相关属性。\n // 悬停以查看现有属性的描述。\n // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387\n \"ve"
},
{
"path": ".vscode/settings.json",
"chars": 660,
"preview": "{\n \"[javascript]\": {\n \"editor.defaultIndentSize\": 2,\n \"editor.tabSize\": 2\n },\n \"[typescript]\": {\n \"editor.de"
},
{
"path": ".vscode/toolkit.code-snippets",
"chars": 1287,
"preview": "{\n \"appendElement - full\": {\n \"scope\": \"javascript,typescript\",\n \"prefix\": \"appendElement\",\n \"body\": [\n \""
},
{
"path": "LICENSE",
"chars": 34520,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "README.md",
"chars": 2018,
"preview": "<div align=center>\n\n\n\n# 茉莉花 Jasminum\n\n[,海外用户请取消勾选\nlabel-"
},
{
"path": "addon/locale/zh-CN/preferences-translators.ftl",
"chars": 233,
"preview": "title = 中文社区转换器列表\n\ngithub-link = \n .label = 项目主页\n\nsearch-box =\n .placeholder = 搜索转换器\n\n# Links\nhow-to-update-translator"
},
{
"path": "addon/locale/zh-CN/progress.ftl",
"chars": 157,
"preview": "title = 茉莉花任务窗口\ntask-list = 任务列表\nresult-source = 来源:{ source }\nresult-title = 标题:{ title }\nresult-score = 匹配度:{ score }\n"
},
{
"path": "addon/locale/zh-TW/addon.ftl",
"chars": 2007,
"preview": "plugin-name = 茉莉花\n\nprefs-table-title = 標題\nprefs-table-detail = 詳細資料\ntabpanel-lib-tab-label = 圖書館標籤\ntabpanel-reader-tab-l"
},
{
"path": "addon/locale/zh-TW/mainWindow.ftl",
"chars": 337,
"preview": "item-section-example1-head-text =\n .label = 插件模板: 条目信息\nitem-section-example1-sidenav-tooltip =\n .tooltiptext = 这是插"
},
{
"path": "addon/locale/zh-TW/preferences-main.ftl",
"chars": 2864,
"preview": "# 元數據設定\npref-group-metadata = 中文元數據抓取設定\nlabel-isMainlandChina = \n .label = 目前位於中國大陸(不包括中國香港、中國澳門及中國台灣),海外用戶請取消勾選\nlabel-"
},
{
"path": "addon/locale/zh-TW/preferences-translators.ftl",
"chars": 232,
"preview": "title = 中文社群轉換器列表\n\ngithub-link =\n .label = 專案首頁\n\nsearch-box =\n .placeholder = 搜尋轉換器\n\n# Links\nhow-to-update-translator"
},
{
"path": "addon/locale/zh-TW/progress.ftl",
"chars": 160,
"preview": "title = 茉莉花任務視窗\ntask-list = 任務列表\nresult-source = 來源:{ source }\nresult-title = 標題:{ title }\nresult-score = 匹配度:{ score }\n"
},
{
"path": "addon/manifest.json",
"chars": 535,
"preview": "{\n \"manifest_version\": 2,\n \"name\": \"__addonName__\",\n \"version\": \"__buildVersion__\",\n \"description\": \"__descr"
},
{
"path": "addon/prefs.js",
"chars": 844,
"preview": "/* eslint-disable no-undef */\npref(\"firstRun\", true);\npref(\"translatorsMended\", false);\n/* tools */\npref(\"autoSplitName\""
},
{
"path": "doc/README-zhCN.md",
"chars": 16060,
"preview": "# Zotero Plugin Template\n\n[ => {\n console.log(\"Minimal Worker收到:\", e"
},
{
"path": "src/modules/workers/outline.ts",
"chars": 4470,
"preview": "import {\n PDFArray,\n PDFDict,\n PDFDocument,\n PDFHexString,\n PDFName,\n PDFNull,\n PDFNumber,\n PDFPageLeaf,\n PDFRe"
},
{
"path": "src/modules/wps.ts",
"chars": 2338,
"preview": "async function unZip(filename: string, outDir: string) {\n ztoolkit.log(outDir, filename);\n const zipFile = Zotero.File"
},
{
"path": "src/utils/cookiebox.ts",
"chars": 10299,
"preview": "export class MyCookieSandbox {\n public searchCookieBox: Zotero.CookieSandbox | null = null;\n // public attachmentCoo"
},
{
"path": "src/utils/detect.ts",
"chars": 1293,
"preview": "// 这里有许多类型判断,判断不同的条目类型\n\n/**\n * 主要检测知网等其他数据库下载的附件文件名是否至少有3个汉字\n * Created by DeepSeek\n * @param filename\n * @returns\n */\n\n"
},
{
"path": "src/utils/http.ts",
"chars": 3045,
"preview": "function jsonToFormUrlEncoded(json: any) {\n return Object.keys(json)\n .map(\n (key) =>\n encodeURIComponen"
},
{
"path": "src/utils/locale.ts",
"chars": 2638,
"preview": "import { config } from \"../../package.json\";\n\nexport { initLocale, getString, getLocaleID };\n\n/**\n * Initialize locale d"
},
{
"path": "src/utils/pattern.ts",
"chars": 2913,
"preview": "export function getArgsFromPattern(\n filename: string,\n pattern: string,\n): SearchOption | null {\n // Make query para"
},
{
"path": "src/utils/pdfParser.ts",
"chars": 13398,
"preview": "async function getPDFTitle(itemID: number): Promise<string> {\n // @ts-ignore - PDFWorker is not typed\n const recognize"
},
{
"path": "src/utils/prefs.ts",
"chars": 840,
"preview": "import { config } from \"../../package.json\";\n\nexport type PluginPrefsMap = _ZoteroTypes.Prefs[\"PluginPrefsMap\"];\n\nconst "
},
{
"path": "src/utils/task.ts",
"chars": 7554,
"preview": "import { metaSearch, metaTranslate } from \"../modules/services\";\nimport { getString } from \"./locale\";\nimport { attachme"
},
{
"path": "src/utils/wait.ts",
"chars": 1251,
"preview": "/**\n * Wait until the condition is `true` or timeout.\n * The callback is triggered if condition returns `true`.\n * @para"
},
{
"path": "src/utils/window.ts",
"chars": 3157,
"preview": "import { config } from \"../../package.json\";\nimport { waitUtilAsync } from \"./wait\";\n\n/**\n * Check if the window is aliv"
},
{
"path": "src/utils/ztoolkit.ts",
"chars": 1349,
"preview": "import { ZoteroToolkit } from \"zotero-plugin-toolkit\";\nimport { config } from \"../../package.json\";\n\nexport { createZToo"
},
{
"path": "test/CNKI_translator_test.js",
"chars": 3758,
"preview": "urls = [\n \"https://kns.cnki.net/kcms2/article/abstract?v=WStw-PbchoxXklMd97G3EDrMY35-fvcPWOGNHjK8hkbbqADLh5NGc0AmBzjI4D"
},
{
"path": "test/expert_china.json",
"chars": 1370,
"preview": "{\n \"boolSearch\": \"true\",\n \"QueryJson\": {\n \"Platform\": \"\",\n \"Resource\": \"CROSSDB\",\n \"Classid\": \"WD0FTY92\",\n "
},
{
"path": "test/expert_oversea.json",
"chars": 1260,
"preview": "{\n \"IsSearch\": \"true\",\n \"QueryJson\": {\n \"Platform\": \"\",\n \"DBCode\": \"CFLS\",\n \"KuaKuCode\": \"CJFQ,CDMD,CIPD,CCND"
},
{
"path": "tsconfig.json",
"chars": 288,
"preview": "{\n \"compilerOptions\": {\n \"experimentalDecorators\": true,\n \"module\": \"commonjs\",\n \"target\": \"ES2016\",\n \"reso"
},
{
"path": "typings/attachment.d.ts",
"chars": 423,
"preview": "interface AttachmentService {\n searchAttachments(\n task: AttachmentTask,\n ): Promise<AttachmentSearchResult[] | nul"
},
{
"path": "typings/global.d.ts",
"chars": 401,
"preview": "declare const _globalThis: {\n [key: string]: any;\n Zotero: _ZoteroTypes.Zotero;\n ztoolkit: ZToolkit;\n addon: typeof "
},
{
"path": "typings/i10n.d.ts",
"chars": 3370,
"preview": "// Generated by zotero-plugin-scaffold\n/* prettier-ignore */\n/* eslint-disable */\n// @ts-nocheck\nexport type FluentMessa"
},
{
"path": "typings/myzotero.d.ts",
"chars": 5335,
"preview": "declare namespace Zotero {\n /**\n * Cookie 对象的内部存储结构\n */\n interface CookieData {\n /** Cookie 的值 */\n value: st"
},
{
"path": "typings/notifier.d.ts",
"chars": 0,
"preview": ""
},
{
"path": "typings/outline.d.ts",
"chars": 1261,
"preview": "type OutlineNode = {\n level: number;\n title: string;\n page: number;\n x: number;\n y: number;\n children?: OutlineNod"
},
{
"path": "typings/pdfParser.d.ts",
"chars": 1500,
"preview": "type RecognizerData = {\n metadata: {\n [key: string]: string;\n };\n totalPages: number;\n pages: RecognizerPage[];\n}"
},
{
"path": "typings/prefs.d.ts",
"chars": 971,
"preview": "// Generated by zotero-plugin-scaffold\n/* prettier-ignore */\n/* eslint-disable */\n// @ts-nocheck\n\n// prettier-ignore\ndec"
},
{
"path": "typings/scrape.d.ts",
"chars": 1275,
"preview": "interface ScrapeService {\n search(searchOption: SearchOption): Promise<ScrapeSearchResult[] | null>;\n searchSnapshot?("
},
{
"path": "typings/translators.d.ts",
"chars": 203,
"preview": "type LastUpdatedMap = {\n [filename: string]: { label: string; lastUpdated: string };\n};\n\ntype TableRow = {\n filename: "
},
{
"path": "zotero-plugin.config.ts",
"chars": 1554,
"preview": "import { defineConfig } from \"zotero-plugin-scaffold\";\nimport pkg from \"./package.json\";\n\nexport default defineConfig({\n"
}
]
About this extraction
This page contains the full source code of the l0o0/jasminum GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 88 files (373.6 KB), approximately 108.1k tokens, and a symbol index with 295 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.